From 9a91bee0abad54d9fee3f3956901d48dfc33cad7 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 3 Sep 2025 16:09:24 +0200 Subject: [PATCH 1/4] chore(ffi): Remove the old latest event API. --- bindings/matrix-sdk-ffi/src/room/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room/mod.rs b/bindings/matrix-sdk-ffi/src/room/mod.rs index 1ce0ec8d32a..5f219f87c53 100644 --- a/bindings/matrix-sdk-ffi/src/room/mod.rs +++ b/bindings/matrix-sdk-ffi/src/room/mod.rs @@ -48,7 +48,7 @@ use crate::{ runtime::get_runtime_handle, timeline::{ configuration::{TimelineConfiguration, TimelineFilter}, - EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline, + LatestEventValue, ReceiptType, SendHandle, Timeline, }, utils::{u64_to_uint, AsyncRuntimeDropped}, TaskHandle, @@ -300,10 +300,6 @@ impl Room { .unwrap_or(false) } - async fn latest_event(&self) -> Option { - self.inner.latest_event_item().await.map(Into::into) - } - async fn new_latest_event(&self) -> LatestEventValue { self.inner.new_latest_event().await.into() } From 50377bfc2d04b07e6cae863b9673105f4ba63105 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 3 Sep 2025 16:30:06 +0200 Subject: [PATCH 2/4] chore(ui): Remove the old latest event API. So satisfying. --- .../src/timeline/event_item/content/mod.rs | 235 +------- .../src/timeline/event_item/mod.rs | 513 +----------------- crates/matrix-sdk-ui/src/timeline/mod.rs | 9 - .../matrix-sdk-ui/src/timeline/tests/mod.rs | 8 +- crates/matrix-sdk-ui/src/timeline/traits.rs | 31 +- .../tests/integration/room_list_service.rs | 69 +-- .../integration/timeline/read_receipts.rs | 4 +- 7 files changed, 32 insertions(+), 837 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index ae44f45c35a..3c74613ad1d 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -16,21 +16,15 @@ use std::sync::Arc; use as_variant::as_variant; use matrix_sdk::crypto::types::events::UtdCause; -use matrix_sdk_base::latest_event::{PossibleLatestEvent, is_suitable_for_latest_event}; use ruma::{ OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedUserId, UserId, events::{ - AnyFullStateEventContent, AnySyncTimelineEvent, FullStateEventContent, Mentions, - MessageLikeEventType, StateEventType, - call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent}, + AnyFullStateEventContent, FullStateEventContent, Mentions, MessageLikeEventType, + StateEventType, policy::rule::{ room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent, user::PolicyRuleUserEventContent, }, - poll::unstable_start::{ - NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, - UnstablePollStartEventContent, - }, room::{ aliases::RoomAliasesEventContent, avatar::RoomAvatarEventContent, @@ -41,23 +35,22 @@ use ruma::{ guest_access::RoomGuestAccessEventContent, history_visibility::RoomHistoryVisibilityEventContent, join_rules::RoomJoinRulesEventContent, - member::{Change, RoomMemberEventContent, SyncRoomMemberEvent}, - message::{MessageType, Relation, SyncRoomMessageEvent}, + member::{Change, RoomMemberEventContent}, + message::MessageType, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, - power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, + power_levels::RoomPowerLevelsEventContent, server_acl::RoomServerAclEventContent, third_party_invite::RoomThirdPartyInviteEventContent, tombstone::RoomTombstoneEventContent, topic::RoomTopicEventContent, }, space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, - sticker::{StickerEventContent, SyncStickerEvent}, + sticker::StickerEventContent, }, html::RemoveReplyFallback, room_version_rules::RedactionRules, }; -use tracing::warn; mod message; mod msg_like; @@ -121,222 +114,6 @@ pub enum TimelineItemContent { } impl TimelineItemContent { - /// If the supplied event is suitable to be used as a `latest_event` in a - /// message preview, extract its contents and wrap it as a - /// `TimelineItemContent`. - pub(crate) fn from_latest_event_content( - event: AnySyncTimelineEvent, - power_levels_info: Option<(&UserId, &RoomPowerLevels)>, - ) -> Option { - match is_suitable_for_latest_event(&event, power_levels_info) { - PossibleLatestEvent::YesRoomMessage(m) => { - Some(Self::from_suitable_latest_event_content(m)) - } - PossibleLatestEvent::YesSticker(s) => { - Some(Self::from_suitable_latest_sticker_content(s)) - } - PossibleLatestEvent::YesPoll(poll) => { - Some(Self::from_suitable_latest_poll_event_content(poll)) - } - PossibleLatestEvent::YesCallInvite(call_invite) => { - Some(Self::from_suitable_latest_call_invite_content(call_invite)) - } - PossibleLatestEvent::YesCallNotify(call_notify) => { - Some(Self::from_suitable_latest_call_notify_content(call_notify)) - } - PossibleLatestEvent::NoUnsupportedEventType => { - // TODO: when we support state events in message previews, this will need change - warn!("Found a state event cached as latest_event! ID={}", event.event_id()); - None - } - PossibleLatestEvent::NoUnsupportedMessageLikeType => { - // TODO: When we support reactions in message previews, this will need to change - warn!( - "Found an event cached as latest_event, but I don't know how \ - to wrap it in a TimelineItemContent. type={}, ID={}", - event.event_type().to_string(), - event.event_id() - ); - None - } - PossibleLatestEvent::YesKnockedStateEvent(member) => { - Some(Self::from_suitable_latest_knock_state_event_content(member)) - } - PossibleLatestEvent::NoEncrypted => { - warn!("Found an encrypted event cached as latest_event! ID={}", event.event_id()); - None - } - } - } - - /// Given some message content that is from an event that we have already - /// determined is suitable for use as a latest event in a message preview, - /// extract its contents and wrap it as a `TimelineItemContent`. - fn from_suitable_latest_event_content(event: &SyncRoomMessageEvent) -> TimelineItemContent { - match event { - SyncRoomMessageEvent::Original(event) => { - // Grab the content of this event - let event_content = event.content.clone(); - - // Feed the bundled edit, if present, or we might miss showing edited content. - let edit = event - .unsigned - .relations - .replace - .as_ref() - .and_then(|boxed| match &boxed.content.relates_to { - Some(Relation::Replacement(re)) => Some(re.new_content.clone()), - _ => { - warn!("got m.room.message event with an edit without a valid m.replace relation"); - None - } - }); - - // We're not interested in aggregations for the latest preview item. - let reactions = Default::default(); - let thread_root = None; - let in_reply_to = None; - let thread_summary = None; - - let msglike = MsgLikeContent { - kind: MsgLikeKind::Message(Message::from_event( - event_content.msgtype, - event_content.mentions, - edit, - RemoveReplyFallback::Yes, - )), - reactions, - thread_root, - in_reply_to, - thread_summary, - }; - - TimelineItemContent::MsgLike(msglike) - } - - SyncRoomMessageEvent::Redacted(_) => { - TimelineItemContent::MsgLike(MsgLikeContent::redacted()) - } - } - } - - fn from_suitable_latest_knock_state_event_content( - event: &SyncRoomMemberEvent, - ) -> TimelineItemContent { - match event { - SyncRoomMemberEvent::Original(event) => { - let content = event.content.clone(); - let prev_content = event.prev_content().cloned(); - TimelineItemContent::room_member( - event.state_key.to_owned(), - FullStateEventContent::Original { content, prev_content }, - event.sender.to_owned(), - ) - } - SyncRoomMemberEvent::Redacted(_) => { - TimelineItemContent::MsgLike(MsgLikeContent::redacted()) - } - } - } - - /// Given some sticker content that is from an event that we have already - /// determined is suitable for use as a latest event in a message preview, - /// extract its contents and wrap it as a `TimelineItemContent`. - fn from_suitable_latest_sticker_content(event: &SyncStickerEvent) -> TimelineItemContent { - match event { - SyncStickerEvent::Original(event) => { - // Grab the content of this event - let event_content = event.content.clone(); - - // We're not interested in aggregations for the latest preview item. - let reactions = Default::default(); - let thread_root = None; - let in_reply_to = None; - let thread_summary = None; - - let msglike = MsgLikeContent { - kind: MsgLikeKind::Sticker(Sticker { content: event_content }), - reactions, - thread_root, - in_reply_to, - thread_summary, - }; - - TimelineItemContent::MsgLike(msglike) - } - SyncStickerEvent::Redacted(_) => { - TimelineItemContent::MsgLike(MsgLikeContent::redacted()) - } - } - } - - /// Extracts a `TimelineItemContent` from a poll start event for use as a - /// latest event in a message preview. - fn from_suitable_latest_poll_event_content( - event: &SyncUnstablePollStartEvent, - ) -> TimelineItemContent { - let SyncUnstablePollStartEvent::Original(event) = event else { - return TimelineItemContent::MsgLike(MsgLikeContent::redacted()); - }; - - // Feed the bundled edit, if present, or we might miss showing edited content. - let edit = - event.unsigned.relations.replace.as_ref().and_then(|boxed| match &boxed.content { - UnstablePollStartEventContent::Replacement(re) => { - Some(re.relates_to.new_content.clone()) - } - _ => { - warn!("got poll event with an edit without a valid m.replace relation"); - None - } - }); - - let mut poll = PollState::new(NewUnstablePollStartEventContent::new( - event.content.poll_start().clone(), - )); - if let Some(edit) = edit { - poll = poll.edit(edit).expect("the poll can't be ended yet!"); // TODO or can it? - } - - // We're not interested in aggregations for the latest preview item. - let reactions = Default::default(); - let thread_root = None; - let in_reply_to = None; - let thread_summary = None; - - let msglike = MsgLikeContent { - kind: MsgLikeKind::Poll(poll), - reactions, - thread_root, - in_reply_to, - thread_summary, - }; - - TimelineItemContent::MsgLike(msglike) - } - - fn from_suitable_latest_call_invite_content( - event: &SyncCallInviteEvent, - ) -> TimelineItemContent { - match event { - SyncCallInviteEvent::Original(_) => TimelineItemContent::CallInvite, - SyncCallInviteEvent::Redacted(_) => { - TimelineItemContent::MsgLike(MsgLikeContent::redacted()) - } - } - } - - fn from_suitable_latest_call_notify_content( - event: &SyncCallNotifyEvent, - ) -> TimelineItemContent { - match event { - SyncCallNotifyEvent::Original(_) => TimelineItemContent::CallNotify, - SyncCallNotifyEvent::Redacted(_) => { - TimelineItemContent::MsgLike(MsgLikeContent::redacted()) - } - } - } - pub fn as_msglike(&self) -> Option<&MsgLikeContent> { as_variant!(self, TimelineItemContent::MsgLike) } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 5ee62619c4d..78b16d6858b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -20,23 +20,19 @@ use std::{ use as_variant::as_variant; use indexmap::IndexMap; use matrix_sdk::{ - Client, Error, + Error, deserialized_responses::{EncryptionInfo, ShieldState}, send_queue::{SendHandle, SendReactionHandle}, }; -use matrix_sdk_base::{ - deserialized_responses::{SENT_IN_CLEAR, ShieldStateCode}, - latest_event::LatestEvent, -}; +use matrix_sdk_base::deserialized_responses::{SENT_IN_CLEAR, ShieldStateCode}; use once_cell::sync::Lazy; use ruma::{ EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId, - OwnedUserId, RoomId, TransactionId, UserId, + OwnedUserId, TransactionId, UserId, events::{AnySyncTimelineEvent, receipt::Receipt, room::message::MessageType}, room_version_rules::RedactionRules, serde::Raw, }; -use tracing::warn; use unicode_segmentation::UnicodeSegmentation; mod content; @@ -123,96 +119,6 @@ impl EventTimelineItem { Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted } } - /// If the supplied low-level [`TimelineEvent`] is suitable for use as the - /// `latest_event` in a message preview, wrap it as an - /// `EventTimelineItem`. - /// - /// **Note:** Timeline items created via this constructor do **not** produce - /// the correct ShieldState when calling - /// [`get_shield`][EventTimelineItem::get_shield]. This is because they are - /// intended for display in the room list which a) is unlikely to show - /// shields and b) would incur a significant performance overhead. - /// - /// [`TimelineEvent`]: matrix_sdk::deserialized_responses::TimelineEvent - pub async fn from_latest_event( - client: Client, - room_id: &RoomId, - latest_event: LatestEvent, - ) -> Option { - // TODO: We shouldn't be returning an EventTimelineItem here because we're - // starting to diverge on what kind of data we need. The note above is a - // potential footgun which could one day turn into a security issue. - use super::traits::RoomDataProvider; - - let raw_sync_event = latest_event.event().raw().clone(); - let encryption_info = latest_event.event().encryption_info().cloned(); - - let Ok(event) = raw_sync_event.deserialize() else { - warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!"); - return None; - }; - - let timestamp = event.origin_server_ts(); - let sender = event.sender().to_owned(); - let event_id = event.event_id().to_owned(); - let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false); - - // Get the room's power levels for calculating the latest event - let power_levels = if let Some(room) = client.get_room(room_id) { - room.power_levels().await.ok() - } else { - None - }; - let room_power_levels_info = client.user_id().zip(power_levels.as_ref()); - - // If we don't (yet) know how to handle this type of message, return `None` - // here. If we do, convert it into a `TimelineItemContent`. - let content = - TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?; - - // The message preview probably never needs read receipts. - let read_receipts = IndexMap::new(); - - // Being highlighted is _probably_ not relevant to the message preview. - let is_highlighted = false; - - // We may need this, depending on how we are going to display edited messages in - // previews. - let latest_edit_json = None; - - // Probably the origin of the event doesn't matter for the preview. - let origin = RemoteEventOrigin::Sync; - - let kind = RemoteEventTimelineItem { - event_id, - transaction_id: None, - read_receipts, - is_own, - is_highlighted, - encryption_info, - original_json: Some(raw_sync_event), - latest_edit_json, - origin, - } - .into(); - - let room = client.get_room(room_id); - let sender_profile = if let Some(room) = room { - let mut profile = room.profile_from_latest_event(&latest_event); - - // Fallback to the slow path. - if profile.is_none() { - profile = room.profile_from_user_id(&sender).await; - } - - profile.map(TimelineDetails::Ready).unwrap_or(TimelineDetails::Unavailable) - } else { - TimelineDetails::Unavailable - }; - - Some(Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted: false }) - } - /// Check whether this item is a local echo. /// /// This returns `true` for events created locally, until the server echoes @@ -786,416 +692,3 @@ impl ReactionsByKeyBySender { None } } - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use assert_matches2::assert_let; - use matrix_sdk::test_utils::logged_in_client; - use matrix_sdk_base::{ - MinimalStateEvent, OriginalMinimalStateEvent, RequestedRequiredStates, - deserialized_responses::TimelineEvent, latest_event::LatestEvent, - }; - use matrix_sdk_test::{async_test, event_factory::EventFactory, sync_state_event}; - use ruma::{ - RoomId, UInt, UserId, - api::client::sync::sync_events::v5 as http, - event_id, - events::{ - AnySyncStateEvent, - room::{ - member::RoomMemberEventContent, - message::{MessageFormat, MessageType}, - }, - }, - room_id, - serde::Raw, - user_id, - }; - - use super::{EventTimelineItem, Profile}; - use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent}; - - #[async_test] - async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() { - // Given a sync event that is suitable to be used as a latest_event - - let room_id = room_id!("!q:x.uk"); - let user_id = user_id!("@t:o.uk"); - let event = EventFactory::new() - .room(room_id) - .text_html("**My M**", "My M") - .sender(user_id) - .server_ts(122344) - .into_event(); - let client = logged_in_client(None).await; - - // When we construct a timeline event from it - let timeline_item = - EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) - .await - .unwrap(); - - // Then its properties correctly translate - assert_eq!(timeline_item.sender, user_id); - assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable); - assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap()); - if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() { - assert_eq!(txt.body, "**My M**"); - let formatted = txt.formatted.as_ref().unwrap(); - assert_eq!(formatted.format, MessageFormat::Html); - assert_eq!(formatted.body, "My M"); - } else { - panic!("Unexpected message type"); - } - } - - #[async_test] - async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() { - // Given a sync knock member state event that is suitable to be used as a - // latest_event - - let room_id = room_id!("!q:x.uk"); - let user_id = user_id!("@t:o.uk"); - let raw_event = member_event_as_state_event( - room_id, - user_id, - "knock", - "Alice Margatroid", - "mxc://e.org/SEs", - ); - let client = logged_in_client(None).await; - - // Add create and power levels state event, otherwise the knock state event - // can't be used as the latest event - let create_event = sync_state_event!({ - "type": "m.room.create", - "content": { "room_version": "11" }, - "event_id": "$143278582443PhrSm:example.org", - "origin_server_ts": 143273580, - "room_id": room_id, - "sender": user_id, - "state_key": "", - "unsigned": { - "age": 1235 - } - }); - let power_level_event = sync_state_event!({ - "type": "m.room.power_levels", - "content": {}, - "event_id": "$143278582443PhrSn:example.org", - "origin_server_ts": 143273581, - "room_id": room_id, - "sender": user_id, - "state_key": "", - "unsigned": { - "age": 1234 - } - }); - let mut room = http::response::Room::new(); - room.required_state.extend([create_event, power_level_event]); - - // And the room is stored in the client so it can be extracted when needed - let response = response_with_room(room_id, room); - client - .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default()) - .await - .unwrap(); - - // When we construct a timeline event from it - let event = TimelineEvent::from_plaintext(raw_event.cast()); - let timeline_item = - EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) - .await - .unwrap(); - - // Then its properties correctly translate - assert_eq!(timeline_item.sender, user_id); - assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable); - assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap()); - if let TimelineItemContent::MembershipChange(change) = timeline_item.content { - assert_eq!(change.user_id, user_id); - assert_matches!(change.change, Some(MembershipChange::Knocked)); - } else { - panic!("Unexpected state event type"); - } - } - - #[async_test] - async fn test_latest_message_includes_bundled_edit() { - // Given a sync event that is suitable to be used as a latest_event, and - // contains a bundled edit, - let room_id = room_id!("!q:x.uk"); - let user_id = user_id!("@t:o.uk"); - - let f = EventFactory::new(); - - let original_event_id = event_id!("$original"); - - let event = f - .text_html("**My M**", "My M") - .sender(user_id) - .event_id(original_event_id) - .with_bundled_edit( - f.text_html(" * Updated!", " * Updated!") - .edit( - original_event_id, - MessageType::text_html("Updated!", "Updated!").into(), - ) - .event_id(event_id!("$edit")) - .sender(user_id), - ) - .server_ts(42) - .into_event(); - - let client = logged_in_client(None).await; - - // When we construct a timeline event from it, - let timeline_item = - EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) - .await - .unwrap(); - - // Then its properties correctly translate. - assert_eq!(timeline_item.sender, user_id); - assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable); - assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap()); - if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() { - assert_eq!(txt.body, "Updated!"); - let formatted = txt.formatted.as_ref().unwrap(); - assert_eq!(formatted.format, MessageFormat::Html); - assert_eq!(formatted.body, "Updated!"); - } else { - panic!("Unexpected message type"); - } - } - - #[async_test] - async fn test_latest_poll_includes_bundled_edit() { - // Given a sync event that is suitable to be used as a latest_event, and - // contains a bundled edit, - let room_id = room_id!("!q:x.uk"); - let user_id = user_id!("@t:o.uk"); - - let f = EventFactory::new(); - - let original_event_id = event_id!("$original"); - - let event = f - .poll_start( - "It's one avocado, Michael, how much could it cost? 10 dollars?", - "It's one avocado, Michael, how much could it cost?", - vec!["1 dollar", "10 dollars", "100 dollars"], - ) - .event_id(original_event_id) - .with_bundled_edit( - f.poll_edit( - original_event_id, - "It's one banana, Michael, how much could it cost?", - vec!["1 dollar", "10 dollars", "100 dollars"], - ) - .event_id(event_id!("$edit")) - .sender(user_id), - ) - .sender(user_id) - .into_event(); - - let client = logged_in_client(None).await; - - // When we construct a timeline event from it, - let timeline_item = - EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) - .await - .unwrap(); - - // Then its properties correctly translate. - assert_eq!(timeline_item.sender, user_id); - - let poll = timeline_item.content().as_poll().unwrap(); - assert!(poll.has_been_edited); - assert_eq!( - poll.start_event_content.poll_start.question.text, - "It's one banana, Michael, how much could it cost?" - ); - } - - #[async_test] - async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage() - { - // Given a sync event that is suitable to be used as a latest_event, and a room - // with a member event for the sender - - use ruma::owned_mxc_uri; - let room_id = room_id!("!q:x.uk"); - let user_id = user_id!("@t:o.uk"); - let event = EventFactory::new() - .room(room_id) - .text_html("**My M**", "My M") - .sender(user_id) - .into_event(); - let client = logged_in_client(None).await; - let mut room = http::response::Room::new(); - room.required_state.push(member_event_as_state_event( - room_id, - user_id, - "join", - "Alice Margatroid", - "mxc://e.org/SEs", - )); - - // And the room is stored in the client so it can be extracted when needed - let response = response_with_room(room_id, room); - client - .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default()) - .await - .unwrap(); - - // When we construct a timeline event from it - let timeline_item = - EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) - .await - .unwrap(); - - // Then its sender is properly populated - assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile); - assert_eq!( - profile, - Profile { - display_name: Some("Alice Margatroid".to_owned()), - display_name_ambiguous: false, - avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs")) - } - ); - } - - #[async_test] - async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache() - { - // Given a sync event that is suitable to be used as a latest_event, a room, and - // a member event for the sender (which isn't part of the room yet). - - use ruma::owned_mxc_uri; - let room_id = room_id!("!q:x.uk"); - let user_id = user_id!("@t:o.uk"); - let f = EventFactory::new().room(room_id); - let event = f.text_html("**My M**", "My M").sender(user_id).into_event(); - let client = logged_in_client(None).await; - - let member_event = MinimalStateEvent::Original( - f.member(user_id) - .sender(user_id!("@example:example.org")) - .avatar_url("mxc://e.org/SEs".into()) - .display_name("Alice Margatroid") - .reason("") - .into_raw_sync() - .deserialize_as_unchecked::>() - .unwrap(), - ); - - let room = http::response::Room::new(); - // Do not push the `member_event` inside the room. Let's say it's flying in the - // `StateChanges`. - - // And the room is stored in the client so it can be extracted when needed - let response = response_with_room(room_id, room); - client - .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default()) - .await - .unwrap(); - - // When we construct a timeline event from it - let timeline_item = EventTimelineItem::from_latest_event( - client, - room_id, - LatestEvent::new_with_sender_details(event, Some(member_event), None), - ) - .await - .unwrap(); - - // Then its sender is properly populated - assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile); - assert_eq!( - profile, - Profile { - display_name: Some("Alice Margatroid".to_owned()), - display_name_ambiguous: false, - avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs")) - } - ); - } - - #[async_test] - async fn test_emoji_detection() { - let room_id = room_id!("!q:x.uk"); - let user_id = user_id!("@t:o.uk"); - let client = logged_in_client(None).await; - let f = EventFactory::new().room(room_id).sender(user_id); - - let mut event = f.text_html("πŸ€·β€β™‚οΈ No boost πŸ€·β€β™‚οΈ", "").into_event(); - let mut timeline_item = - EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event)) - .await - .unwrap(); - - assert!(!timeline_item.contains_only_emojis()); - - // Ignores leading and trailing white spaces - event = f.text_html(" πŸš€ ", "").into_event(); - timeline_item = - EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event)) - .await - .unwrap(); - - assert!(timeline_item.contains_only_emojis()); - - // Too many - event = f.text_html("πŸ‘¨β€πŸ‘©β€πŸ‘¦1οΈβƒ£πŸš€πŸ‘³πŸΎβ€β™‚οΈπŸͺ©πŸ‘πŸ‘πŸ»πŸ«±πŸΌβ€πŸ«²πŸΎπŸ™‚πŸ‘‹", "").into_event(); - timeline_item = - EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event)) - .await - .unwrap(); - - assert!(!timeline_item.contains_only_emojis()); - - // Works with combined emojis - event = f.text_html("πŸ‘¨β€πŸ‘©β€πŸ‘¦1οΈβƒ£πŸ‘³πŸΎβ€β™‚οΈπŸ‘πŸ»πŸ«±πŸΌβ€πŸ«²πŸΎ", "").into_event(); - timeline_item = - EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event)) - .await - .unwrap(); - - assert!(timeline_item.contains_only_emojis()); - } - - fn member_event_as_state_event( - room_id: &RoomId, - user_id: &UserId, - membership: &str, - display_name: &str, - avatar_url: &str, - ) -> Raw { - sync_state_event!({ - "type": "m.room.member", - "content": { - "avatar_url": avatar_url, - "displayname": display_name, - "membership": membership, - "reason": "" - }, - "event_id": "$143273582443PhrSn:example.org", - "origin_server_ts": 143273583, - "room_id": room_id, - "sender": user_id, - "state_key": user_id, - "unsigned": { - "age": 1234 - } - }) - } - - fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response { - let mut response = http::Response::new("6".to_owned()); - response.rooms.insert(room_id.to_owned(), room); - response - } -} diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index b4bb04da819..263af2102f8 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -254,15 +254,6 @@ impl Timeline { Some(item.to_owned()) } - /// Get the latest of the timeline's event items. - pub async fn latest_event(&self) -> Option { - if self.controller.is_live() { - self.controller.items().await.last()?.as_event().cloned() - } else { - None - } - } - /// Get the current timeline items, along with a stream of updates of /// timeline items. /// diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index eeba31223da..82088401c4c 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -35,9 +35,7 @@ use matrix_sdk::{ room::{EventWithContextResponse, Messages, MessagesOptions, PushContext, Relations}, send_queue::RoomSendQueueUpdate, }; -use matrix_sdk_base::{ - RoomInfo, RoomState, crypto::types::events::CryptoContextInfo, latest_event::LatestEvent, -}; +use matrix_sdk_base::{RoomInfo, RoomState, crypto::types::events::CryptoContextInfo}; use matrix_sdk_test::{ALICE, DEFAULT_TEST_ROOM_ID, event_factory::EventFactory}; use ruma::{ EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedTransactionId, @@ -379,10 +377,6 @@ impl RoomDataProvider for TestRoomDataProvider { None } - fn profile_from_latest_event(&self, _latest_event: &LatestEvent) -> Option { - None - } - async fn load_user_receipt<'a>( &'a self, receipt_type: ReceiptType, diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index b4211c9745d..25311fcc9cf 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -23,7 +23,7 @@ use matrix_sdk::{ paginators::{PaginableRoom, thread::PaginableThread}, room::PushContext, }; -use matrix_sdk_base::{RoomInfo, latest_event::LatestEvent}; +use matrix_sdk_base::RoomInfo; use ruma::{ EventId, OwnedEventId, OwnedTransactionId, OwnedUserId, UserId, events::{ @@ -37,7 +37,7 @@ use ruma::{ }; use tracing::error; -use super::{EventTimelineItem, Profile, RedactError, TimelineBuilder}; +use super::{Profile, RedactError, TimelineBuilder}; use crate::timeline::{ self, Timeline, latest_event::LatestEventValue, pinned_events_loader::PinnedEventsRoom, }; @@ -63,12 +63,6 @@ pub trait RoomExt { /// constructing it. fn timeline_builder(&self) -> TimelineBuilder; - /// Return an optional [`EventTimelineItem`] corresponding to this room's - /// latest event. - fn latest_event_item( - &self, - ) -> impl Future> + SendOutsideWasm; - /// Return a [`LatestEventValue`] corresponding to this room's latest event. fn new_latest_event(&self) -> impl Future; } @@ -82,14 +76,6 @@ impl RoomExt for Room { TimelineBuilder::new(self).track_read_marker_and_receipts() } - async fn latest_event_item(&self) -> Option { - if let Some(latest_event) = self.latest_event() { - EventTimelineItem::from_latest_event(self.client(), self.room_id(), latest_event).await - } else { - None - } - } - async fn new_latest_event(&self) -> LatestEventValue { LatestEventValue::from_base_latest_event_value( (**self).new_latest_event(), @@ -113,7 +99,6 @@ pub(super) trait RoomDataProvider: &'a self, user_id: &'a UserId, ) -> impl Future> + SendOutsideWasm + 'a; - fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option; /// Loads a user receipt from the storage backend. fn load_user_receipt<'a>( @@ -195,18 +180,6 @@ impl RoomDataProvider for Room { } } - fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option { - if !latest_event.has_sender_profile() { - return None; - } - - Some(Profile { - display_name: latest_event.sender_display_name().map(ToOwned::to_owned), - display_name_ambiguous: latest_event.sender_name_ambiguous().unwrap_or(false), - avatar_url: latest_event.sender_avatar_url().map(ToOwned::to_owned), - }) - } - async fn load_user_receipt<'a>( &'a self, receipt_type: ReceiptType, diff --git a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs index 0ceb2d8d373..0bc0f14adce 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -1,4 +1,4 @@ -use std::{ops::Not, sync::Arc}; +use std::sync::Arc; use assert_matches::assert_matches; use eyeball_im::VectorDiff; @@ -22,11 +22,10 @@ use matrix_sdk_ui::{ ALL_ROOMS_LIST_NAME as ALL_ROOMS, Error, RoomListLoadingState, State, SyncIndicator, filters::{new_filter_fuzzy_match_room_name, new_filter_non_left, new_filter_none}, }, - timeline::{RoomExt as _, TimelineItemKind, VirtualTimelineItem}, + timeline::{LatestEventValue, RoomExt, TimelineItemKind, VirtualTimelineItem}, }; use ruma::{ api::client::room::create_room::v3::Request as CreateRoomRequest, - event_id, events::room::message::RoomMessageEventContent, mxc_uri, room_id, time::{Duration, Instant}, @@ -2547,7 +2546,9 @@ async fn test_room_empty_timeline() { #[async_test] async fn test_room_latest_event() -> Result<(), Error> { - let (_, server, room_list) = new_room_list_service().await?; + let (client, server, room_list) = new_room_list_service().await?; + client.event_cache().subscribe().unwrap(); + mock_encryption_state(&server, false).await; let sync = room_list.sync(); @@ -2576,8 +2577,14 @@ async fn test_room_latest_event() -> Result<(), Error> { let room = room_list.room(room_id)?; let timeline = room.timeline_builder().build().await.unwrap(); + // We could subscribe to the room β€”with `RoomList::subscribe_to_rooms`β€” to + // automatically listen to the latest event updates, but we will do it + // manually here (so that we can ignore the subscription thingies). + let latest_events = client.latest_events().await; + latest_events.listen_to_room(room_id).await.unwrap(); + // The latest event does not exist. - assert!(room.latest_event_item().await.is_none()); + assert_matches!(room.new_latest_event().await, LatestEventValue::None); sync_then_assert_request_and_fake_response! { [server, room_list, sync] @@ -2595,58 +2602,20 @@ async fn test_room_latest_event() -> Result<(), Error> { }, }; - // The latest event exists. - assert_matches!( - room.latest_event_item().await, - Some(event) => { - assert!(event.is_local_echo().not()); - assert_eq!(event.event_id(), Some(event_id!("$x0:bar.org"))); - } - ); - - sync_then_assert_request_and_fake_response! { - [server, room_list, sync] - assert request >= {}, - respond with = { - "pos": "2", - "lists": {}, - "rooms": { - room_id: { - "timeline": [ - timeline_event!("$x1:bar.org" at 1 sec), - ], - }, - }, - }, - }; - - // The latest event has been updated. - let latest_event = room.latest_event_item().await.unwrap(); - assert!(latest_event.is_local_echo().not()); - assert_eq!(latest_event.event_id(), Some(event_id!("$x1:bar.org"))); + // Let the latest event be computed. + yield_now().await; - // The latest event matches the latest event of the `Timeline`. - assert_matches!( - timeline.latest_event().await, - Some(timeline_event) => { - assert_eq!(timeline_event.event_id(), latest_event.event_id()); - } - ); + // The latest event exists. + assert_matches!(room.new_latest_event().await, LatestEventValue::Remote { .. }); // Insert a local event in the `Timeline`. timeline.send(RoomMessageEventContent::text_plain("Hello, World!").into()).await.unwrap(); - // Let the send queue send the message, and the timeline process it. + // Let the latest event be computed. yield_now().await; - // The latest event of the `Timeline` is a local event. - assert_matches!( - timeline.latest_event().await, - Some(timeline_event) => { - assert!(timeline_event.is_local_echo()); - assert_eq!(timeline_event.event_id(), None); - } - ); + // The latest event has been updated. + assert_matches!(room.new_latest_event().await, LatestEventValue::Local { .. }); Ok(()) } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs index 10d483d349e..bd98fcccd75 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs @@ -992,9 +992,7 @@ async fn test_mark_as_read() { .await; // And I try to mark the latest event related to a timeline item as read, - let latest_event = timeline.latest_event().await.expect("missing timeline event item"); - let latest_event_id = - latest_event.event_id().expect("missing event id for latest timeline event item"); + let latest_event_id = original_event_id; let has_sent = timeline .send_single_receipt(CreateReceiptType::Read, latest_event_id.to_owned()) From fb997d76e93439ae8995927c5d99f69a0ce4fa03 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 3 Sep 2025 16:51:46 +0200 Subject: [PATCH 3/4] chore(base): Remove the old latest event API. --- crates/matrix-sdk-base/src/client.rs | 25 +- crates/matrix-sdk-base/src/latest_event.rs | 693 +----------------- .../src/response_processors/e2ee/to_device.rs | 10 +- .../src/response_processors/latest_event.rs | 204 ------ .../src/response_processors/mod.rs | 2 - .../response_processors/room/msc4186/mod.rs | 157 ---- .../matrix-sdk-base/src/room/latest_event.rs | 273 +------ crates/matrix-sdk-base/src/room/mod.rs | 22 - crates/matrix-sdk-base/src/room/room_info.rs | 47 +- crates/matrix-sdk-base/src/sliding_sync.rs | 629 +--------------- .../src/store/migration_helpers.rs | 5 +- .../src/helpers.rs | 5 - .../src/tests/sliding_sync/room.rs | 114 --- 13 files changed, 20 insertions(+), 2166 deletions(-) delete mode 100644 crates/matrix-sdk-base/src/response_processors/latest_event.rs diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 38a8e5f01fd..bce633ac035 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -597,35 +597,14 @@ impl BaseClient { let mut context = Context::new(StateChanges::new(response.next_batch.clone())); #[cfg(feature = "e2e-encryption")] - let to_device = { - let processors::e2ee::to_device::Output { - processed_to_device_events: to_device, - room_key_updates, - } = processors::e2ee::to_device::from_sync_v2( + let processors::e2ee::to_device::Output { processed_to_device_events: to_device, .. } = + processors::e2ee::to_device::from_sync_v2( &response, olm_machine.as_ref(), &self.decryption_settings, ) .await?; - processors::latest_event::decrypt_from_rooms( - &mut context, - room_key_updates - .into_iter() - .flatten() - .filter_map(|room_key_info| self.get_room(&room_key_info.room_id)) - .collect(), - processors::e2ee::E2EE::new( - olm_machine.as_ref(), - &self.decryption_settings, - self.handle_verification_events, - ), - ) - .await?; - - to_device - }; - #[cfg(not(feature = "e2e-encryption"))] let to_device = response .to_device diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 6dc4dd88a69..f0dd72dea9f 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -2,26 +2,10 @@ //! use as a [crate::Room::latest_event]. use matrix_sdk_common::deserialized_responses::TimelineEvent; -use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId}; -#[cfg(feature = "e2e-encryption")] -use ruma::{ - UserId, - events::{ - AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, - call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent}, - poll::unstable_start::SyncUnstablePollStartEvent, - relation::RelationType, - room::{ - member::{MembershipState, SyncRoomMemberEvent}, - message::{MessageType, SyncRoomMessageEvent}, - power_levels::RoomPowerLevels, - }, - sticker::SyncStickerEvent, - }, -}; +use ruma::MilliSecondsSinceUnixEpoch; use serde::{Deserialize, Serialize}; -use crate::{MinimalRoomMemberEvent, store::SerializableEventContent}; +use crate::store::SerializableEventContent; /// A latest event value! #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -54,676 +38,3 @@ pub struct LocalLatestEventValue { /// The content of the local event. pub content: SerializableEventContent, } - -/// Represents a decision about whether an event could be stored as the latest -/// event in a room. Variants starting with Yes indicate that this message could -/// be stored, and provide the inner event information, and those starting with -/// a No indicate that it could not, and give a reason. -#[cfg(feature = "e2e-encryption")] -#[derive(Debug)] -pub enum PossibleLatestEvent<'a> { - /// This message is suitable - it is an m.room.message - YesRoomMessage(&'a SyncRoomMessageEvent), - /// This message is suitable - it is a sticker - YesSticker(&'a SyncStickerEvent), - /// This message is suitable - it is a poll - YesPoll(&'a SyncUnstablePollStartEvent), - - /// This message is suitable - it is a call invite - YesCallInvite(&'a SyncCallInviteEvent), - - /// This message is suitable - it's a call notification - YesCallNotify(&'a SyncCallNotifyEvent), - - /// This state event is suitable - it's a knock membership change - /// that can be handled by the current user. - YesKnockedStateEvent(&'a SyncRoomMemberEvent), - - // Later: YesState(), - // Later: YesReaction(), - /// Not suitable - it's a state event - NoUnsupportedEventType, - /// Not suitable - it's not a m.room.message or an edit/replacement - NoUnsupportedMessageLikeType, - /// Not suitable - it's encrypted - NoEncrypted, -} - -/// Decide whether an event could be stored as the latest event in a room. -/// Returns a LatestEvent representing our decision. -#[cfg(feature = "e2e-encryption")] -pub fn is_suitable_for_latest_event<'a>( - event: &'a AnySyncTimelineEvent, - power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>, -) -> PossibleLatestEvent<'a> { - match event { - // Suitable - we have an m.room.message that was not redacted or edited - AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => { - if let Some(original_message) = message.as_original() { - // Don't show incoming verification requests - if let MessageType::VerificationRequest(_) = original_message.content.msgtype { - return PossibleLatestEvent::NoUnsupportedMessageLikeType; - } - - // Check if this is a replacement for another message. If it is, ignore it - let is_replacement = - original_message.content.relates_to.as_ref().is_some_and(|relates_to| { - if let Some(relation_type) = relates_to.rel_type() { - relation_type == RelationType::Replacement - } else { - false - } - }); - - if is_replacement { - PossibleLatestEvent::NoUnsupportedMessageLikeType - } else { - PossibleLatestEvent::YesRoomMessage(message) - } - } else { - PossibleLatestEvent::YesRoomMessage(message) - } - } - - AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => { - PossibleLatestEvent::YesPoll(poll) - } - - AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => { - PossibleLatestEvent::YesCallInvite(invite) - } - - AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(notify)) => { - PossibleLatestEvent::YesCallNotify(notify) - } - - AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => { - PossibleLatestEvent::YesSticker(sticker) - } - - // Encrypted events are not suitable - AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => { - PossibleLatestEvent::NoEncrypted - } - - // Later, if we support reactions: - // AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Reaction(_)) - - // MessageLike, but not one of the types we want to show in message previews, so not - // suitable - AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType, - - // We don't currently support most state events - AnySyncTimelineEvent::State(state) => { - // But we make an exception for knocked state events *if* the current user - // can either accept or decline them - if let AnySyncStateEvent::RoomMember(member) = state - && matches!(member.membership(), MembershipState::Knock) - { - let can_accept_or_decline_knocks = match power_levels_info { - Some((own_user_id, room_power_levels)) => { - room_power_levels.user_can_invite(own_user_id) - || room_power_levels.user_can_kick(own_user_id) - } - _ => false, - }; - - // The current user can act on the knock changes, so they should be - // displayed - if can_accept_or_decline_knocks { - return PossibleLatestEvent::YesKnockedStateEvent(member); - } - } - PossibleLatestEvent::NoUnsupportedEventType - } - } -} - -/// Represent all information required to represent a latest event in an -/// efficient way. -/// -/// ## Implementation details -/// -/// Serialization and deserialization should be a breeze, but we introduced a -/// change in the format without realizing, and without a migration. Ideally, -/// this would be handled with a `serde(untagged)` enum that would be used to -/// deserialize in either the older format, or to the new format. Unfortunately, -/// untagged enums don't play nicely with `serde_json::value::RawValue`, -/// so we did have to implement a custom `Deserialize` for `LatestEvent`, that -/// first deserializes the thing as a raw JSON value, and then deserializes the -/// JSON string as one variant or the other. -/// -/// Because of that, `LatestEvent` should only be (de)serialized using -/// serde_json. -/// -/// Whenever you introduce new fields to `LatestEvent` make sure to add them to -/// `SerializedLatestEvent` too. -#[derive(Clone, Debug, Serialize)] -pub struct LatestEvent { - /// The actual event. - event: TimelineEvent, - - /// The member profile of the event' sender. - #[serde(skip_serializing_if = "Option::is_none")] - sender_profile: Option, - - /// The name of the event' sender is ambiguous. - #[serde(skip_serializing_if = "Option::is_none")] - sender_name_is_ambiguous: Option, -} - -#[derive(Deserialize)] -struct SerializedLatestEvent { - /// The actual event. - event: TimelineEvent, - - /// The member profile of the event' sender. - #[serde(skip_serializing_if = "Option::is_none")] - sender_profile: Option, - - /// The name of the event' sender is ambiguous. - #[serde(skip_serializing_if = "Option::is_none")] - sender_name_is_ambiguous: Option, -} - -// Note: this deserialize implementation for LatestEvent will *only* work with -// serde_json. -impl<'de> Deserialize<'de> for LatestEvent { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let raw: Box = Box::deserialize(deserializer)?; - - let mut variant_errors = Vec::new(); - - match serde_json::from_str::(raw.get()) { - Ok(value) => { - return Ok(LatestEvent { - event: value.event, - sender_profile: value.sender_profile, - sender_name_is_ambiguous: value.sender_name_is_ambiguous, - }); - } - Err(err) => variant_errors.push(err), - } - - match serde_json::from_str::(raw.get()) { - Ok(value) => { - return Ok(LatestEvent { - event: value, - sender_profile: None, - sender_name_is_ambiguous: None, - }); - } - Err(err) => variant_errors.push(err), - } - - Err(serde::de::Error::custom(format!( - "data did not match any variant of serialized LatestEvent (using serde_json). \ - Observed errors: {variant_errors:?}" - ))) - } -} - -impl LatestEvent { - /// Create a new [`LatestEvent`] without the sender's profile. - pub fn new(event: TimelineEvent) -> Self { - Self { event, sender_profile: None, sender_name_is_ambiguous: None } - } - - /// Create a new [`LatestEvent`] with maybe the sender's profile. - pub fn new_with_sender_details( - event: TimelineEvent, - sender_profile: Option, - sender_name_is_ambiguous: Option, - ) -> Self { - Self { event, sender_profile, sender_name_is_ambiguous } - } - - /// Transform [`Self`] into an event. - pub fn into_event(self) -> TimelineEvent { - self.event - } - - /// Get a reference to the event. - pub fn event(&self) -> &TimelineEvent { - &self.event - } - - /// Get a mutable reference to the event. - pub fn event_mut(&mut self) -> &mut TimelineEvent { - &mut self.event - } - - /// Get the event ID. - pub fn event_id(&self) -> Option { - self.event.event_id() - } - - /// Check whether [`Self`] has a sender profile. - pub fn has_sender_profile(&self) -> bool { - self.sender_profile.is_some() - } - - /// Return the sender's display name if it was known at the time [`Self`] - /// was built. - pub fn sender_display_name(&self) -> Option<&str> { - self.sender_profile.as_ref().and_then(|profile| { - profile.as_original().and_then(|event| event.content.displayname.as_deref()) - }) - } - - /// Return `Some(true)` if the sender's name is ambiguous, `Some(false)` if - /// it isn't, `None` if ambiguity detection wasn't possible at the time - /// [`Self`] was built. - pub fn sender_name_ambiguous(&self) -> Option { - self.sender_name_is_ambiguous - } - - /// Return the sender's avatar URL if it was known at the time [`Self`] was - /// built. - pub fn sender_avatar_url(&self) -> Option<&MxcUri> { - self.sender_profile.as_ref().and_then(|profile| { - profile.as_original().and_then(|event| event.content.avatar_url.as_deref()) - }) - } -} - -#[cfg(test)] -mod tests { - #[cfg(feature = "e2e-encryption")] - use std::collections::BTreeMap; - - #[cfg(feature = "e2e-encryption")] - use assert_matches::assert_matches; - #[cfg(feature = "e2e-encryption")] - use assert_matches2::assert_let; - use matrix_sdk_common::deserialized_responses::TimelineEvent; - use ruma::serde::Raw; - #[cfg(feature = "e2e-encryption")] - use ruma::{ - MilliSecondsSinceUnixEpoch, UInt, VoipVersionId, - events::{ - AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey, - Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent, - RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent, - call::{ - SessionDescription, - invite::{CallInviteEventContent, SyncCallInviteEvent}, - notify::{ - ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent, - }, - }, - poll::{ - unstable_response::{ - SyncUnstablePollResponseEvent, UnstablePollResponseEventContent, - }, - unstable_start::{ - NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, - UnstablePollAnswer, UnstablePollStartContentBlock, - }, - }, - relation::Replacement, - room::{ - ImageInfo, MediaSource, - encrypted::{ - EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent, - SyncRoomEncryptedEvent, - }, - message::{ - ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent, - Relation, RoomMessageEventContent, SyncRoomMessageEvent, - }, - topic::{RoomTopicEventContent, SyncRoomTopicEvent}, - }, - sticker::{StickerEventContent, SyncStickerEvent}, - }, - owned_event_id, owned_mxc_uri, owned_user_id, - }; - use serde_json::json; - - use super::LatestEvent; - #[cfg(feature = "e2e-encryption")] - use super::{PossibleLatestEvent, is_suitable_for_latest_event}; - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_room_messages_are_suitable() { - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent { - content: RoomMessageEventContent::new(MessageType::Image( - ImageMessageEventContent::new( - "".to_owned(), - MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")), - ), - )), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - assert_let!( - PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event, None) - ); - - assert_eq!(m.content.msgtype.msgtype(), "m.image"); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_polls_are_suitable() { - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart( - SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent { - content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new( - "do you like rust?", - vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(), - )) - .into(), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - assert_let!( - PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event, None) - ); - - assert_eq!(m.content.poll_start().question.text, "do you like rust?"); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_call_invites_are_suitable() { - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite( - SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent { - content: CallInviteEventContent::new( - "call_id".into(), - UInt::new(123).unwrap(), - SessionDescription::new("".into(), "".into()), - VoipVersionId::V1, - ), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - assert_let!( - PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event, None) - ); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_call_notifications_are_suitable() { - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify( - SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent { - content: CallNotifyEventContent::new( - "call_id".into(), - ApplicationType::Call, - NotifyType::Ring, - Mentions::new(), - ), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - assert_let!( - PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event, None) - ); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_stickers_are_suitable() { - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker( - SyncStickerEvent::Original(OriginalSyncMessageLikeEvent { - content: StickerEventContent::new( - "sticker!".to_owned(), - ImageInfo::new(), - owned_mxc_uri!("mxc://example.com/1"), - ), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - - assert_matches!( - is_suitable_for_latest_event(&event, None), - PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_)) - ); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_different_types_of_messagelike_are_unsuitable() { - let event = - AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse( - SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent { - content: UnstablePollResponseEventContent::new( - vec![String::from("option1")], - owned_event_id!("$1"), - ), - event_id: owned_event_id!("$2"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - - assert_matches!( - is_suitable_for_latest_event(&event, None), - PossibleLatestEvent::NoUnsupportedMessageLikeType - ); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_redacted_messages_are_suitable() { - // Ruma does not allow constructing UnsignedRoomRedactionEvent instances. - let room_redaction_event = serde_json::from_value(json!({ - "content": {}, - "event_id": "$redaction", - "sender": "@x:y.za", - "origin_server_ts": 223543, - "unsigned": { "reason": "foo" } - })) - .unwrap(); - - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent { - content: RedactedRoomMessageEventContent::new(), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: RedactedUnsigned::new(room_redaction_event), - }), - )); - - assert_matches!( - is_suitable_for_latest_event(&event, None), - PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_)) - ); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_encrypted_messages_are_unsuitable() { - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted( - SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent { - content: RoomEncryptedEventContent::new( - EncryptedEventScheme::OlmV1Curve25519AesSha2( - OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()), - ), - None, - ), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - - assert_matches!( - is_suitable_for_latest_event(&event, None), - PossibleLatestEvent::NoEncrypted - ); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_state_events_are_unsuitable() { - let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic( - SyncRoomTopicEvent::Original(OriginalSyncStateEvent { - content: RoomTopicEventContent::new("".to_owned()), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: StateUnsigned::new(), - state_key: EmptyStateKey, - }), - )); - - assert_matches!( - is_suitable_for_latest_event(&event, None), - PossibleLatestEvent::NoUnsupportedEventType - ); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_replacement_events_are_unsuitable() { - let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!"); - event_content.relates_to = Some(Relation::Replacement(Replacement::new( - owned_event_id!("$1"), - RoomMessageEventContent::text_plain("Hello, world!").into(), - ))); - - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent { - content: event_content, - event_id: owned_event_id!("$2"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - - assert_matches!( - is_suitable_for_latest_event(&event, None), - PossibleLatestEvent::NoUnsupportedMessageLikeType - ); - } - - #[cfg(feature = "e2e-encryption")] - #[test] - fn test_verification_requests_are_unsuitable() { - use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id}; - - let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent { - content: RoomMessageEventContent::new(MessageType::VerificationRequest( - KeyVerificationRequestEventContent::new( - "body".to_owned(), - vec![], - device_id!("device_id").to_owned(), - user_id!("@user_id:example.com").to_owned(), - ), - )), - event_id: owned_event_id!("$1"), - sender: owned_user_id!("@a:b.c"), - origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()), - unsigned: MessageLikeUnsigned::new(), - }), - )); - - assert_let!( - PossibleLatestEvent::NoUnsupportedMessageLikeType = - is_suitable_for_latest_event(&event, None) - ); - } - - #[test] - fn test_deserialize_latest_event() { - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct TestStruct { - latest_event: LatestEvent, - } - - let event = TimelineEvent::from_plaintext( - Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(), - ); - - let initial = TestStruct { - latest_event: LatestEvent { - event: event.clone(), - sender_profile: None, - sender_name_is_ambiguous: None, - }, - }; - - // When serialized, LatestEvent always uses the new format. - let serialized = serde_json::to_value(&initial).unwrap(); - assert_eq!( - serialized, - json!({ - "latest_event": { - "event": { - "kind": { - "PlainText": { - "event": { - "event_id": "$1" - } - } - }, - "thread_summary": "None", - } - } - }) - ); - - // And it can be properly deserialized from the new format. - let deserialized: TestStruct = serde_json::from_value(serialized).unwrap(); - assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1"); - assert!(deserialized.latest_event.sender_profile.is_none()); - assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none()); - - // The previous format can also be deserialized. - let serialized = json!({ - "latest_event": { - "event": { - "encryption_info": null, - "event": { - "event_id": "$1" - } - }, - } - }); - - let deserialized: TestStruct = serde_json::from_value(serialized).unwrap(); - assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1"); - assert!(deserialized.latest_event.sender_profile.is_none()); - assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none()); - - // The even older format can also be deserialized. - let serialized = json!({ - "latest_event": event - }); - - let deserialized: TestStruct = serde_json::from_value(serialized).unwrap(); - assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1"); - assert!(deserialized.latest_event.sender_profile.is_none()); - assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none()); - } -} diff --git a/crates/matrix-sdk-base/src/response_processors/e2ee/to_device.rs b/crates/matrix-sdk-base/src/response_processors/e2ee/to_device.rs index f30a3085254..635ff3f6df4 100644 --- a/crates/matrix-sdk-base/src/response_processors/e2ee/to_device.rs +++ b/crates/matrix-sdk-base/src/response_processors/e2ee/to_device.rs @@ -17,9 +17,7 @@ use std::collections::BTreeMap; use matrix_sdk_common::deserialized_responses::{ ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo, ToDeviceUnableToDecryptReason, }; -use matrix_sdk_crypto::{ - DecryptionSettings, EncryptionSyncChanges, OlmMachine, store::types::RoomKeyInfo, -}; +use matrix_sdk_crypto::{DecryptionSettings, EncryptionSyncChanges, OlmMachine}; use ruma::{ OneTimeKeyAlgorithm, UInt, api::client::sync::sync_events::{DeviceLists, v3, v5}, @@ -100,10 +98,10 @@ async fn process( // decrypts to-device events, but leaves room events alone. // This makes sure that we have the decryption keys for the room // events at hand. - let (events, room_key_updates) = + let (events, _room_key_updates) = olm_machine.receive_sync_changes(encryption_sync_changes, decryption_settings).await?; - Output { processed_to_device_events: events, room_key_updates: Some(room_key_updates) } + Output { processed_to_device_events: events } } else { // If we have no `OlmMachine`, just return the clear events that were passed in. // The encrypted ones are dropped as they are un-usable. @@ -131,12 +129,10 @@ async fn process( } }) .collect(), - room_key_updates: None, } }) } pub struct Output { pub processed_to_device_events: Vec, - pub room_key_updates: Option>, } diff --git a/crates/matrix-sdk-base/src/response_processors/latest_event.rs b/crates/matrix-sdk-base/src/response_processors/latest_event.rs deleted file mode 100644 index b51b64cf406..00000000000 --- a/crates/matrix-sdk-base/src/response_processors/latest_event.rs +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2025 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use matrix_sdk_common::deserialized_responses::TimelineEvent; -use matrix_sdk_crypto::RoomEventDecryptionResult; -use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw}; - -use super::{Context, e2ee::E2EE, verification}; -use crate::{ - Result, Room, - latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event}, -}; - -/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of -/// [`Room`]s. -/// -/// If we can decrypt them, change [`Room::latest_event`] to reflect what we -/// found, and remove any older encrypted events from -/// [`Room::latest_encrypted_events`]. -pub async fn decrypt_from_rooms( - context: &mut Context, - rooms: Vec, - e2ee: E2EE<'_>, -) -> Result<()> { - // All functions used by this one expect an `OlmMachine`. Return if there is - // none. - if e2ee.olm_machine.is_none() { - return Ok(()); - } - - for room in rooms { - // Try to find a message we can decrypt and is suitable for using as the latest - // event. If we found one, set it as the latest and delete any older - // encrypted events - if let Some((found, found_index)) = find_suitable_and_decrypt(&room, &e2ee).await { - room.on_latest_event_decrypted( - found, - found_index, - &mut context.state_changes, - &mut context.room_info_notable_updates, - ); - } - } - - Ok(()) -} - -async fn find_suitable_and_decrypt( - room: &Room, - e2ee: &E2EE<'_>, -) -> Option<(Box, usize)> { - let enc_events = room.latest_encrypted_events(); - let power_levels = room.power_levels().await.ok(); - let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref()); - - // Walk backwards through the encrypted events, looking for one we can decrypt - for (i, event) in enc_events.iter().enumerate().rev() { - // Size of the `decrypt_sync_room_event` future should not impact this - // async fn since it is likely that there aren't even any encrypted - // events when calling it. - let decrypt_sync_room_event = - Box::pin(decrypt_sync_room_event(event, e2ee, room.room_id())); - - if let Ok(decrypted) = decrypt_sync_room_event.await { - // We found an event we can decrypt - if let Ok(any_sync_event) = decrypted.raw().deserialize() { - // We can deserialize it to find its type - match is_suitable_for_latest_event(&any_sync_event, power_levels_info) { - PossibleLatestEvent::YesRoomMessage(_) - | PossibleLatestEvent::YesPoll(_) - | PossibleLatestEvent::YesCallInvite(_) - | PossibleLatestEvent::YesCallNotify(_) - | PossibleLatestEvent::YesSticker(_) - | PossibleLatestEvent::YesKnockedStateEvent(_) => { - return Some((Box::new(LatestEvent::new(decrypted)), i)); - } - _ => (), - } - } - } - } - - None -} - -/// Attempt to decrypt the given raw event into a [`TimelineEvent`]. -/// -/// In the case of a decryption error, returns a [`TimelineEvent`] -/// representing the decryption error; in the case of problems with our -/// application, returns `Err`. -/// -/// # Panics -/// -/// Panics if there is no [`OlmMachine`] in [`E2EE`]. -async fn decrypt_sync_room_event( - event: &Raw, - e2ee: &E2EE<'_>, - room_id: &RoomId, -) -> Result { - let event = match e2ee - .olm_machine - .expect("An `OlmMachine` is expected") - .try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings) - .await? - { - RoomEventDecryptionResult::Decrypted(decrypted) => { - // We're fine not setting the push actions for the latest event. - let event = TimelineEvent::from_decrypted(decrypted, None); - - if let Ok(sync_timeline_event) = event.raw().deserialize() { - verification::process_if_relevant(&sync_timeline_event, e2ee.clone(), room_id) - .await?; - } - - event - } - - RoomEventDecryptionResult::UnableToDecrypt(utd_info) => { - TimelineEvent::from_utd(event.clone(), utd_info) - } - }; - - Ok(event) -} - -#[cfg(test)] -mod tests { - use matrix_sdk_test::{ - JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory, - }; - use ruma::{event_id, events::room::member::MembershipState, room_id, user_id}; - - use super::{Context, E2EE, decrypt_from_rooms}; - use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client}; - - #[async_test] - async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() { - // Given a room - let user_id = user_id!("@u:u.to"); - let room_id = room_id!("!r:u.to"); - - let client = logged_in_base_client(Some(user_id)).await; - - let mut sync_builder = SyncResponseBuilder::new(); - - let response = sync_builder - .add_joined_room( - JoinedRoomBuilder::new(room_id).add_timeline_event( - EventFactory::new() - .member(user_id) - .display_name("Alice") - .membership(MembershipState::Join) - .event_id(event_id!("$1")), - ), - ) - .build_sync_response(); - client.receive_sync_response(response).await.unwrap(); - - let room = client.get_room(room_id).expect("Just-created room not found!"); - - // Sanity: it has no latest_encrypted_events or latest_event - assert!(room.latest_encrypted_events().is_empty()); - assert!(room.latest_event().is_none()); - - // When I tell it to do some decryption - let mut context = Context::default(); - - decrypt_from_rooms( - &mut context, - vec![room.clone()], - E2EE::new( - client.olm_machine().await.as_ref(), - &client.decryption_settings, - client.handle_verification_events, - ), - ) - .await - .unwrap(); - - // Then nothing changed - assert!(room.latest_encrypted_events().is_empty()); - assert!(room.latest_event().is_none()); - assert!(context.state_changes.room_infos.is_empty()); - assert!( - !context - .room_info_notable_updates - .get(room_id) - .copied() - .unwrap_or_default() - .contains(RoomInfoNotableUpdateReasons::LATEST_EVENT) - ); - } -} diff --git a/crates/matrix-sdk-base/src/response_processors/mod.rs b/crates/matrix-sdk-base/src/response_processors/mod.rs index e074da6b0aa..2985db3b196 100644 --- a/crates/matrix-sdk-base/src/response_processors/mod.rs +++ b/crates/matrix-sdk-base/src/response_processors/mod.rs @@ -17,8 +17,6 @@ pub mod changes; #[cfg(feature = "e2e-encryption")] pub mod e2ee; pub mod ephemeral_events; -#[cfg(feature = "e2e-encryption")] -pub mod latest_event; pub mod notification; pub mod profiles; pub mod room; diff --git a/crates/matrix-sdk-base/src/response_processors/room/msc4186/mod.rs b/crates/matrix-sdk-base/src/response_processors/room/msc4186/mod.rs index 881ed3da3bb..15c196d1ce9 100644 --- a/crates/matrix-sdk-base/src/response_processors/room/msc4186/mod.rs +++ b/crates/matrix-sdk-base/src/response_processors/room/msc4186/mod.rs @@ -18,8 +18,6 @@ use std::collections::BTreeMap; #[cfg(feature = "e2e-encryption")] use std::collections::BTreeSet; -#[cfg(feature = "e2e-encryption")] -use matrix_sdk_common::deserialized_responses::TimelineEvent; use matrix_sdk_common::timer; use ruma::{ JsOption, OwnedRoomId, RoomId, UserId, @@ -42,8 +40,6 @@ use super::{ super::{Context, notification, state_events, timeline}, RoomCreationData, }; -#[cfg(feature = "e2e-encryption")] -use crate::StateChanges; use crate::{ Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState, @@ -154,18 +150,6 @@ pub async fn update_any_room( ) .await?; - // Cache the latest decrypted event in room_info, and also keep any later - // encrypted events, so we can slot them in when we get the keys. - #[cfg(feature = "e2e-encryption")] - cache_latest_events( - &room, - &mut room_info, - &timeline.events, - Some(&context.state_changes), - Some(state_store), - ) - .await; - #[cfg(feature = "e2e-encryption")] e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted( e2ee.olm_machine, @@ -418,147 +402,6 @@ fn properties( } } -/// Find the most recent decrypted event and cache it in the supplied RoomInfo. -/// -/// If any encrypted events are found after that one, store them in the RoomInfo -/// too so we can use them when we get the relevant keys. -/// -/// It is the responsibility of the caller to update the `RoomInfo` instance -/// stored in the `Room`. -#[cfg(feature = "e2e-encryption")] -pub(crate) async fn cache_latest_events( - room: &Room, - room_info: &mut RoomInfo, - events: &[TimelineEvent], - changes: Option<&StateChanges>, - store: Option<&BaseStateStore>, -) { - use tracing::warn; - - use crate::{ - deserialized_responses::DisplayName, - latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event}, - store::ambiguity_map::is_display_name_ambiguous, - }; - - let _timer = timer!(tracing::Level::TRACE, "cache_latest_events"); - - let mut encrypted_events = - Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); - - // Try to get room power levels from the current changes. If we didn't get any - // info, try getting it from local data. - let power_levels = match changes.and_then(|changes| changes.power_levels(room_info.room_id())) { - Some(power_levels) => Some(power_levels), - None => room.power_levels().await.ok(), - }; - - let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref()); - - for event in events.iter().rev() { - if let Ok(timeline_event) = event.raw().deserialize() { - match is_suitable_for_latest_event(&timeline_event, power_levels_info) { - PossibleLatestEvent::YesRoomMessage(_) - | PossibleLatestEvent::YesPoll(_) - | PossibleLatestEvent::YesCallInvite(_) - | PossibleLatestEvent::YesCallNotify(_) - | PossibleLatestEvent::YesSticker(_) - | PossibleLatestEvent::YesKnockedStateEvent(_) => { - // We found a suitable latest event. Store it. - - // In order to make the latest event fast to read, we want to keep the - // associated sender in cache. This is a best-effort to gather enough - // information for creating a user profile as fast as possible. If information - // are missing, let's go back on the β€œslow” path. - - let mut sender_profile = None; - let mut sender_name_is_ambiguous = None; - - // First off, look up the sender's profile from the `StateChanges`, they are - // likely to be the most recent information. - if let Some(changes) = changes { - sender_profile = changes - .profiles - .get(room.room_id()) - .and_then(|profiles_by_user| { - profiles_by_user.get(timeline_event.sender()) - }) - .cloned(); - - if let Some(sender_profile) = sender_profile.as_ref() { - sender_name_is_ambiguous = sender_profile - .as_original() - .and_then(|profile| profile.content.displayname.as_ref()) - .and_then(|display_name| { - let display_name = DisplayName::new(display_name); - - changes.ambiguity_maps.get(room.room_id()).and_then( - |map_for_room| { - map_for_room.get(&display_name).map(|users| { - is_display_name_ambiguous(&display_name, users) - }) - }, - ) - }); - } - } - - // Otherwise, look up the sender's profile from the `Store`. - if sender_profile.is_none() - && let Some(store) = store - { - sender_profile = store - .get_profile(room.room_id(), timeline_event.sender()) - .await - .ok() - .flatten(); - - // TODO: need to update `sender_name_is_ambiguous`, - // but how? - } - - let latest_event = Box::new(LatestEvent::new_with_sender_details( - event.clone(), - sender_profile, - sender_name_is_ambiguous, - )); - - // Store it in the return RoomInfo (it will be saved for us in the room later). - room_info.latest_event = Some(latest_event); - // We don't need any of the older encrypted events because we have a new - // decrypted one. - room.latest_encrypted_events.write().unwrap().clear(); - // We can stop looking through the timeline now because everything else is - // older. - break; - } - PossibleLatestEvent::NoEncrypted => { - // m.room.encrypted - this might be the latest event later - we can't tell until - // we are able to decrypt it, so store it for now - // - // Check how many encrypted events we have seen. Only store another if we - // haven't already stored the maximum number. - if encrypted_events.len() < encrypted_events.capacity() { - encrypted_events.push(event.raw().clone()); - } - } - _ => { - // Ignore unsuitable events - } - } - } else { - warn!( - "Failed to deserialize event as AnySyncTimelineEvent. ID={}", - event.event_id().expect("Event has no ID!") - ); - } - } - - // Push the encrypted events we found into the Room, in reverse order, so - // the latest is last - room.latest_encrypted_events.write().unwrap().extend(encrypted_events.into_iter().rev()); -} - impl State { /// Construct a [`State`] from the state changes for a joined or left room /// from a response of the Simplified Sliding Sync endpoint. diff --git a/crates/matrix-sdk-base/src/room/latest_event.rs b/crates/matrix-sdk-base/src/room/latest_event.rs index d572dfd37df..e1101a74430 100644 --- a/crates/matrix-sdk-base/src/room/latest_event.rs +++ b/crates/matrix-sdk-base/src/room/latest_event.rs @@ -12,283 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "e2e-encryption")] -use std::{collections::BTreeMap, num::NonZeroUsize}; - -#[cfg(feature = "e2e-encryption")] -use ruma::{OwnedRoomId, events::AnySyncTimelineEvent, serde::Raw}; - use super::Room; -#[cfg(feature = "e2e-encryption")] -use super::RoomInfoNotableUpdateReasons; -use crate::latest_event::{LatestEvent, LatestEventValue}; +use crate::latest_event::LatestEventValue; impl Room { - /// The size of the latest_encrypted_events RingBuffer - #[cfg(feature = "e2e-encryption")] - pub(super) const MAX_ENCRYPTED_EVENTS: NonZeroUsize = NonZeroUsize::new(10).unwrap(); - - /// Return the last event in this room, if one has been cached during - /// sliding sync. - pub fn latest_event(&self) -> Option { - self.inner.read().latest_event.as_deref().cloned() - } - /// Return the [`LatestEventValue`] of this room. pub fn new_latest_event(&self) -> LatestEventValue { self.inner.read().new_latest_event.clone() } - - /// Return the most recent few encrypted events. When the keys come through - /// to decrypt these, the most recent relevant one will replace - /// latest_event. (We can't tell which one is relevant until - /// they are decrypted.) - #[cfg(feature = "e2e-encryption")] - pub(crate) fn latest_encrypted_events(&self) -> Vec> { - self.latest_encrypted_events.read().unwrap().iter().cloned().collect() - } - - /// Replace our latest_event with the supplied event, and delete it and all - /// older encrypted events from latest_encrypted_events, given that the - /// new event was at the supplied index in the latest_encrypted_events - /// list. - /// - /// Panics if index is not a valid index in the latest_encrypted_events - /// list. - /// - /// It is the responsibility of the caller to apply the changes into the - /// state store after calling this function. - #[cfg(feature = "e2e-encryption")] - pub(crate) fn on_latest_event_decrypted( - &self, - latest_event: Box, - index: usize, - changes: &mut crate::StateChanges, - room_info_notable_updates: &mut BTreeMap, - ) { - self.latest_encrypted_events.write().unwrap().drain(0..=index); - - let room_info = changes - .room_infos - .entry(self.room_id().to_owned()) - .or_insert_with(|| self.clone_info()); - - room_info.latest_event = Some(latest_event); - - room_info_notable_updates - .entry(self.room_id().to_owned()) - .or_default() - .insert(RoomInfoNotableUpdateReasons::LATEST_EVENT); - } -} - -#[cfg(all(test, feature = "e2e-encryption"))] -mod tests_with_e2e_encryption { - use std::sync::Arc; - - use assert_matches::assert_matches; - use matrix_sdk_common::deserialized_responses::TimelineEvent; - use matrix_sdk_test::async_test; - use ruma::{room_id, serde::Raw, user_id}; - use serde_json::json; - - use crate::{ - BaseClient, Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState, - SessionMeta, StateChanges, - client::ThreadingSupport, - latest_event::LatestEvent, - response_processors as processors, - store::{MemoryStore, RoomLoadSettings, StoreConfig}, - }; - - fn make_room_test_helper(room_type: RoomState) -> (Arc, Room) { - let store = Arc::new(MemoryStore::new()); - let user_id = user_id!("@me:example.org"); - let room_id = room_id!("!test:localhost"); - let (sender, _receiver) = tokio::sync::broadcast::channel(1); - - (store.clone(), Room::new(user_id, store, room_id, room_type, sender)) - } - - #[async_test] - async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() { - // Given a room, - let client = BaseClient::new( - StoreConfig::new("cross-process-store-locks-holder-name".to_owned()), - ThreadingSupport::Disabled, - ); - - client - .activate( - SessionMeta { - user_id: user_id!("@alice:example.org").into(), - device_id: ruma::device_id!("AYEAYEAYE").into(), - }, - RoomLoadSettings::default(), - None, - ) - .await - .unwrap(); - - let room_id = room_id!("!test:localhost"); - let room = client.get_or_create_room(room_id, RoomState::Joined); - - // That has an encrypted event, - add_encrypted_event(&room, "$A"); - // Sanity: it has no latest_event - assert!(room.latest_event().is_none()); - - // When I set up an observer on the latest_event, - let mut room_info_notable_update = client.room_info_notable_update_receiver(); - - // And I provide a decrypted event to replace the encrypted one, - let event = make_latest_event("$A"); - - let mut context = processors::Context::default(); - room.on_latest_event_decrypted( - event.clone(), - 0, - &mut context.state_changes, - &mut context.room_info_notable_updates, - ); - - assert!(context.room_info_notable_updates.contains_key(room_id)); - - // The subscriber isn't notified at this point. - assert!(room_info_notable_update.is_empty()); - - // Then updating the room info will store the event, - processors::changes::save_and_apply( - context, - &client.state_store, - &client.ignore_user_list_changes, - None, - ) - .await - .unwrap(); - - assert_eq!(room.latest_event().unwrap().event_id(), event.event_id()); - - // And wake up the subscriber. - assert_matches!( - room_info_notable_update.recv().await, - Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => { - assert_eq!(received_room_id, room_id); - assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT)); - } - ); - } - - #[async_test] - async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() { - use std::collections::BTreeMap; - - // Given a room with an encrypted event - let (_store, room) = make_room_test_helper(RoomState::Joined); - add_encrypted_event(&room, "$A"); - // Sanity: it has no latest_event - assert!(room.latest_event().is_none()); - - // When I provide a decrypted event to replace the encrypted one - let event = make_latest_event("$A"); - let mut changes = StateChanges::default(); - let mut room_info_notable_updates = BTreeMap::new(); - room.on_latest_event_decrypted( - event.clone(), - 0, - &mut changes, - &mut room_info_notable_updates, - ); - room.set_room_info( - changes.room_infos.get(room.room_id()).cloned().unwrap(), - room_info_notable_updates.get(room.room_id()).copied().unwrap(), - ); - - // Then is it stored - assert_eq!(room.latest_event().unwrap().event_id(), event.event_id()); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() { - // Given a room with some encrypted events and a latest event - - use std::collections::BTreeMap; - let (_store, room) = make_room_test_helper(RoomState::Joined); - room.inner.update(|info| info.latest_event = Some(make_latest_event("$A"))); - add_encrypted_event(&room, "$0"); - add_encrypted_event(&room, "$1"); - add_encrypted_event(&room, "$2"); - add_encrypted_event(&room, "$3"); - - // When I provide a latest event - let new_event = make_latest_event("$1"); - let new_event_index = 1; - let mut changes = StateChanges::default(); - let mut room_info_notable_updates = BTreeMap::new(); - room.on_latest_event_decrypted( - new_event.clone(), - new_event_index, - &mut changes, - &mut room_info_notable_updates, - ); - room.set_room_info( - changes.room_infos.get(room.room_id()).cloned().unwrap(), - room_info_notable_updates.get(room.room_id()).copied().unwrap(), - ); - - // Then the encrypted events list is shortened to only newer events - let enc_evs = room.latest_encrypted_events(); - assert_eq!(enc_evs.len(), 2); - assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2"); - assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3"); - - // And the event is stored - assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id()); - } - - #[async_test] - async fn test_replacing_the_newest_event_leaves_none_left() { - use std::collections::BTreeMap; - - // Given a room with some encrypted events - let (_store, room) = make_room_test_helper(RoomState::Joined); - add_encrypted_event(&room, "$0"); - add_encrypted_event(&room, "$1"); - add_encrypted_event(&room, "$2"); - add_encrypted_event(&room, "$3"); - - // When I provide a latest event and say it was the very latest - let new_event = make_latest_event("$3"); - let new_event_index = 3; - let mut changes = StateChanges::default(); - let mut room_info_notable_updates = BTreeMap::new(); - room.on_latest_event_decrypted( - new_event, - new_event_index, - &mut changes, - &mut room_info_notable_updates, - ); - room.set_room_info( - changes.room_infos.get(room.room_id()).cloned().unwrap(), - room_info_notable_updates.get(room.room_id()).copied().unwrap(), - ); - - // Then the encrypted events list ie empty - let enc_evs = room.latest_encrypted_events(); - assert_eq!(enc_evs.len(), 0); - } - - fn add_encrypted_event(room: &Room, event_id: &str) { - room.latest_encrypted_events - .write() - .unwrap() - .push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap()); - } - - fn make_latest_event(event_id: &str) -> Box { - Box::new(LatestEvent::new(TimelineEvent::from_plaintext( - Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(), - ))) - } } diff --git a/crates/matrix-sdk-base/src/room/mod.rs b/crates/matrix-sdk-base/src/room/mod.rs index d246617beef..c03b60efe51 100644 --- a/crates/matrix-sdk-base/src/room/mod.rs +++ b/crates/matrix-sdk-base/src/room/mod.rs @@ -26,8 +26,6 @@ mod state; mod tags; mod tombstone; -#[cfg(feature = "e2e-encryption")] -use std::sync::RwLock as SyncRwLock; use std::{ collections::{BTreeMap, HashSet}, sync::Arc, @@ -39,8 +37,6 @@ pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName}; pub use encryption::EncryptionState; use eyeball::{AsyncLock, SharedObservable}; use futures_util::{Stream, StreamExt}; -#[cfg(feature = "e2e-encryption")] -use matrix_sdk_common::ring_buffer::RingBuffer; pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships}; pub(crate) use room_info::SyncInfo; pub use room_info::{ @@ -63,8 +59,6 @@ use ruma::{ }, room::RoomType, }; -#[cfg(feature = "e2e-encryption")] -use ruma::{events::AnySyncTimelineEvent, serde::Raw}; use serde::{Deserialize, Serialize}; pub use state::{RoomState, RoomStateFilter}; pub(crate) use tags::RoomNotableTags; @@ -95,18 +89,6 @@ pub struct Room { pub(super) room_info_notable_update_sender: broadcast::Sender, pub(super) store: Arc, - /// The most recent few encrypted events. When the keys come through to - /// decrypt these, the most recent relevant one will replace - /// `latest_event`. (We can't tell which one is relevant until - /// they are decrypted.) - /// - /// Currently, these are held in Room rather than RoomInfo, because we were - /// not sure whether holding too many of them might make the cache too - /// slow to load on startup. Keeping them here means they are not cached - /// to disk but held in memory. - #[cfg(feature = "e2e-encryption")] - pub latest_encrypted_events: Arc>>>, - /// A map for ids of room membership events in the knocking state linked to /// the user id of the user affected by the member event, that the current /// user has marked as seen so they can be ignored. @@ -141,10 +123,6 @@ impl Room { room_id: room_info.room_id.clone(), store, inner: SharedObservable::new(room_info), - #[cfg(feature = "e2e-encryption")] - latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new( - Self::MAX_ENCRYPTED_EVENTS, - ))), room_info_notable_update_sender, seen_knock_request_ids_map: SharedObservable::new_async(None), room_member_updates_sender, diff --git a/crates/matrix-sdk-base/src/room/room_info.rs b/crates/matrix-sdk-base/src/room/room_info.rs index 060d003ea0d..4f4b35186df 100644 --- a/crates/matrix-sdk-base/src/room/room_info.rs +++ b/crates/matrix-sdk-base/src/room/room_info.rs @@ -19,9 +19,7 @@ use std::{ use bitflags::bitflags; use eyeball::Subscriber; -use matrix_sdk_common::{ - ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, deserialized_responses::TimelineEventKind, -}; +use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK}; use ruma::{ EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, @@ -53,7 +51,7 @@ use ruma::{ serde::Raw, }; use serde::{Deserialize, Serialize}; -use tracing::{debug, error, field::debug, info, instrument, warn}; +use tracing::{error, field::debug, info, instrument, warn}; use super::{ AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, @@ -62,7 +60,7 @@ use super::{ use crate::{ MinimalStateEvent, OriginalMinimalStateEvent, deserialized_responses::RawSyncOrStrippedState, - latest_event::{LatestEvent, LatestEventValue}, + latest_event::LatestEventValue, notification_settings::RoomNotificationMode, read_receipts::RoomReadReceipts, store::{DynStateStore, StateStoreExt}, @@ -452,11 +450,6 @@ pub struct RoomInfo { /// Whether or not the encryption info was been synced. pub(crate) encryption_state_synced: bool, - /// The last event send by sliding sync - /// - /// TODO(@hywan): Remove. - pub(crate) latest_event: Option>, - /// The latest event value of this room. /// /// TODO(@hywan): Rename to `latest_event`. @@ -519,7 +512,6 @@ impl RoomInfo { last_prev_batch: None, sync_info: SyncInfo::NoState, encryption_state_synced: false, - latest_event: None, new_latest_event: LatestEventValue::default(), read_receipts: Default::default(), base_info: Box::new(BaseRoomInfo::new()), @@ -720,25 +712,6 @@ impl RoomInfo { }; tracing::Span::current().record("redacts", debug(redacts)); - if let Some(latest_event) = &mut self.latest_event { - tracing::trace!("Checking if redaction applies to latest event"); - if latest_event.event_id().as_deref() == Some(redacts) { - match apply_redaction(latest_event.event().raw(), _raw, &redaction_rules) { - Some(redacted) => { - // Even if the original event was encrypted, redaction removes all its - // fields so it cannot possibly be successfully decrypted after redaction. - latest_event.event_mut().kind = - TimelineEventKind::PlainText { event: redacted }; - debug!("Redacted latest event"); - } - None => { - self.latest_event = None; - debug!("Removed latest event"); - } - } - } - } - self.base_info.handle_redaction(redacts); } @@ -1038,12 +1011,12 @@ impl RoomInfo { .collect() } - /// Returns the latest (decrypted) event recorded for this room. - pub fn latest_event(&self) -> Option<&LatestEvent> { - self.latest_event.as_deref() + /// Return the new [`LatestEventValue`]. + pub fn new_latest_event(&self) -> &LatestEventValue { + &self.new_latest_event } - /// Sets the new `LatestEventValue`. + /// Sets the new [`LatestEventValue`]. pub fn set_new_latest_event(&mut self, new_value: LatestEventValue) { self.new_latest_event = new_value; } @@ -1254,7 +1227,6 @@ mod tests { use std::sync::Arc; use assert_matches::assert_matches; - use matrix_sdk_common::deserialized_responses::TimelineEvent; use matrix_sdk_test::{ async_test, test_json::{TAG, sync_events::PINNED_EVENTS}, @@ -1269,7 +1241,6 @@ mod tests { use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo}; use crate::{ RoomDisplayName, RoomHero, RoomState, StateChanges, - latest_event::LatestEvent, notification_settings::RoomNotificationMode, room::{RoomNotableTags, RoomSummary}, store::{IntoStateStore, MemoryStore}, @@ -1302,9 +1273,6 @@ mod tests { last_prev_batch: Some("pb".to_owned()), sync_info: SyncInfo::FullySynced, encryption_state_synced: true, - latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::from_plaintext( - Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(), - )))), new_latest_event: LatestEventValue::None, base_info: Box::new( assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }), @@ -1539,7 +1507,6 @@ mod tests { assert_eq!(info.last_prev_batch, Some("pb".to_owned())); assert_eq!(info.sync_info, SyncInfo::FullySynced); assert!(info.encryption_state_synced); - assert!(info.latest_event.is_none()); assert_matches!(info.new_latest_event, LatestEventValue::None); assert!(info.base_info.avatar.is_none()); assert!(info.base_info.canonical_alias.is_none()); diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index f91978eaa74..551dd3e1bf8 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -60,9 +60,9 @@ impl BaseClient { let olm_machine = self.olm_machine().await; - let mut context = processors::Context::default(); + let context = processors::Context::default(); - let processors::e2ee::to_device::Output { processed_to_device_events, room_key_updates } = + let processors::e2ee::to_device::Output { processed_to_device_events, .. } = processors::e2ee::to_device::from_msc4186( to_device, e2ee, @@ -71,21 +71,6 @@ impl BaseClient { ) .await?; - processors::latest_event::decrypt_from_rooms( - &mut context, - room_key_updates - .into_iter() - .flatten() - .filter_map(|room_key_info| self.get_room(&room_key_info.room_id)) - .collect(), - processors::e2ee::E2EE::new( - olm_machine.as_ref(), - &self.decryption_settings, - self.handle_verification_events, - ), - ) - .await?; - processors::changes::save_and_apply( context, &self.state_store, @@ -317,31 +302,21 @@ impl BaseClient { #[cfg(all(test, not(target_family = "wasm")))] mod tests { use std::collections::{BTreeMap, HashSet}; - #[cfg(feature = "e2e-encryption")] - use std::sync::{Arc, RwLock as SyncRwLock}; use assert_matches::assert_matches; - use matrix_sdk_common::deserialized_responses::TimelineEvent; - #[cfg(feature = "e2e-encryption")] - use matrix_sdk_common::{ - deserialized_responses::{UnableToDecryptInfo, UnableToDecryptReason}, - ring_buffer::RingBuffer, - }; use matrix_sdk_test::async_test; use ruma::{ JsOption, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId, api::client::sync::sync_events::UnreadNotificationsCount, assign, event_id, events::{ - AnySyncMessageLikeEvent, AnySyncTimelineEvent, GlobalAccountDataEventContent, - StateEventContent, StateEventType, + GlobalAccountDataEventContent, StateEventContent, StateEventType, direct::{DirectEventContent, DirectUserIdentifier, OwnedDirectUserIdentifier}, room::{ avatar::RoomAvatarEventContent, canonical_alias::RoomCanonicalAliasEventContent, encryption::RoomEncryptionEventContent, member::{MembershipState, RoomMemberEventContent}, - message::SyncRoomMessageEvent, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, }, @@ -353,8 +328,6 @@ mod tests { use serde_json::json; use super::http; - #[cfg(feature = "e2e-encryption")] - use super::processors::room::msc4186::cache_latest_events; use crate::{ BaseClient, EncryptionState, RequestedRequiredStates, RoomInfoNotableUpdate, RoomState, SessionMeta, @@ -363,8 +336,6 @@ mod tests { store::{RoomLoadSettings, StoreConfig}, test_utils::logged_in_base_client, }; - #[cfg(feature = "e2e-encryption")] - use crate::{Room, store::MemoryStore}; #[async_test] async fn test_notification_count_set() { @@ -1277,502 +1248,6 @@ mod tests { ); } - #[async_test] - async fn test_last_event_from_sliding_sync_is_cached() { - // Given a logged-in client - let client = logged_in_base_client(None).await; - let room_id = room_id!("!r:e.uk"); - let event_a = json!({ - "sender":"@alice:example.com", - "type":"m.room.message", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content":{"body":"A", "msgtype": "m.text"} - }); - let event_b = json!({ - "sender":"@alice:example.com", - "type":"m.room.message", - "event_id": "$idb", - "origin_server_ts": 12344447, - "content":{"body":"B", "msgtype": "m.text"} - }); - - // When the sliding sync response contains a timeline - let events = &[event_a, event_b.clone()]; - let room = room_with_timeline(events); - let response = response_with_room(room_id, room); - client - .process_sliding_sync(&response, &RequestedRequiredStates::default()) - .await - .expect("Failed to process sync"); - - // Then the room holds the latest event - let client_room = client.get_room(room_id).expect("No room found"); - assert_eq!( - ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())), - "$idb" - ); - } - - #[async_test] - async fn test_last_knock_event_from_sliding_sync_is_cached_if_user_has_permissions() { - let own_user_id = user_id!("@me:e.uk"); - // Given a logged-in client - let client = logged_in_base_client(Some(own_user_id)).await; - let room_id = room_id!("!r:e.uk"); - - // The room create event. - let create = json!({ - "sender":"@ignacio:example.com", - "state_key":"", - "type":"m.room.create", - "event_id": "$idc", - "origin_server_ts": 12344415, - "content":{ "room_version": "11" }, - "room_id": room_id, - }); - - // Give the current user invite or kick permissions in this room - let power_levels = json!({ - "sender":"@alice:example.com", - "state_key":"", - "type":"m.room.power_levels", - "event_id": "$idb", - "origin_server_ts": 12344445, - "content":{ "invite": 100, "kick": 100, "users": { own_user_id: 100 } }, - "room_id": room_id, - }); - - // And a knock member state event - let knock_event = json!({ - "sender":"@alice:example.com", - "state_key":"@alice:example.com", - "type":"m.room.member", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content":{"membership": "knock"}, - "room_id": room_id, - }); - - // When the sliding sync response contains a timeline - let events = &[knock_event]; - let mut room = room_with_timeline(events); - room.required_state.extend([ - Raw::new(&create).unwrap().cast_unchecked(), - Raw::new(&power_levels).unwrap().cast_unchecked(), - ]); - let response = response_with_room(room_id, room); - client - .process_sliding_sync(&response, &RequestedRequiredStates::default()) - .await - .expect("Failed to process sync"); - - // Then the room holds the latest knock state event - let client_room = client.get_room(room_id).expect("No room found"); - assert_eq!( - ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())), - "$ida" - ); - } - - #[async_test] - async fn test_last_knock_event_from_sliding_sync_is_not_cached_without_permissions() { - let own_user_id = user_id!("@me:e.uk"); - // Given a logged-in client - let client = logged_in_base_client(Some(own_user_id)).await; - let room_id = room_id!("!r:e.uk"); - - // Set the user as a user with no permission to invite or kick other users in - // this room - let power_levels = json!({ - "sender":"@alice:example.com", - "state_key":"", - "type":"m.room.power_levels", - "event_id": "$idb", - "origin_server_ts": 12344445, - "content":{ "invite": 50, "kick": 50, "users": { own_user_id: 0 } }, - "room_id": room_id, - }); - - // And a knock member state event - let knock_event = json!({ - "sender":"@alice:example.com", - "state_key":"@alice:example.com", - "type":"m.room.member", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content":{"membership": "knock"}, - "room_id": room_id, - }); - - // When the sliding sync response contains a timeline - let events = &[knock_event]; - let mut room = room_with_timeline(events); - room.required_state.push(Raw::new(&power_levels).unwrap().cast_unchecked()); - let response = response_with_room(room_id, room); - client - .process_sliding_sync(&response, &RequestedRequiredStates::default()) - .await - .expect("Failed to process sync"); - - // Then the room doesn't hold the knock state event as the latest event - let client_room = client.get_room(room_id).expect("No room found"); - assert!(client_room.latest_event().is_none()); - } - - #[async_test] - async fn test_last_non_knock_member_state_event_from_sliding_sync_is_not_cached() { - // Given a logged-in client - let client = logged_in_base_client(None).await; - let room_id = room_id!("!r:e.uk"); - // And a join member state event - let join_event = json!({ - "sender":"@alice:example.com", - "state_key":"@alice:example.com", - "type":"m.room.member", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content":{"membership": "join"}, - "room_id": room_id, - }); - - // When the sliding sync response contains a timeline - let events = &[join_event]; - let room = room_with_timeline(events); - let response = response_with_room(room_id, room); - client - .process_sliding_sync(&response, &RequestedRequiredStates::default()) - .await - .expect("Failed to process sync"); - - // Then the room doesn't hold the join state event as the latest event - let client_room = client.get_room(room_id).expect("No room found"); - assert!(client_room.latest_event().is_none()); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_cached_latest_event_can_be_redacted() { - // Given a logged-in client - let client = logged_in_base_client(None).await; - let room_id = room_id!("!r:e.uk"); - let event_a = json!({ - "sender": "@alice:example.com", - "type": "m.room.message", - "event_id": "$ida", - "origin_server_ts": 12344446, - "content": { "body":"A", "msgtype": "m.text" }, - }); - - // When the sliding sync response contains a timeline - let room = room_with_timeline(&[event_a]); - let response = response_with_room(room_id, room); - client - .process_sliding_sync(&response, &RequestedRequiredStates::default()) - .await - .expect("Failed to process sync"); - - // Then the room holds the latest event - let client_room = client.get_room(room_id).expect("No room found"); - assert_eq!( - ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())), - "$ida" - ); - - let redaction = json!({ - "sender": "@alice:example.com", - "type": "m.room.redaction", - "event_id": "$idb", - "redacts": "$ida", - "origin_server_ts": 12344448, - "content": {}, - }); - - // When a redaction for that event is received - let room = room_with_timeline(&[redaction]); - let response = response_with_room(room_id, room); - client - .process_sliding_sync(&response, &RequestedRequiredStates::default()) - .await - .expect("Failed to process sync"); - - // Then the room still holds the latest event - let client_room = client.get_room(room_id).expect("No room found"); - let latest_event = client_room.latest_event().unwrap(); - assert_eq!(latest_event.event_id().unwrap(), "$ida"); - - // But it's now redacted - assert_matches!( - latest_event.event().raw().deserialize().unwrap(), - AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncRoomMessageEvent::Redacted(_) - )) - ); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_when_no_events_we_dont_cache_any() { - let events = &[]; - let chosen = choose_event_to_cache(events).await; - assert!(chosen.is_none()); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_when_only_one_event_we_cache_it() { - let event1 = make_event("m.room.message", "$1"); - let events = std::slice::from_ref(&event1); - let chosen = choose_event_to_cache(events).await; - assert_eq!(ev_id(chosen), rawev_id(event1)); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_with_multiple_events_we_cache_the_last_one() { - let event1 = make_event("m.room.message", "$1"); - let event2 = make_event("m.room.message", "$2"); - let events = &[event1, event2.clone()]; - let chosen = choose_event_to_cache(events).await; - assert_eq!(ev_id(chosen), rawev_id(event2)); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() { - let event1 = make_event("m.room.message", "$1"); - let event2 = make_event("m.room.message", "$2"); - let event3 = make_event("m.room.powerlevels", "$3"); - let event4 = make_event("m.room.powerlevels", "$5"); - let events = &[event1, event2.clone(), event3, event4]; - let chosen = choose_event_to_cache(events).await; - assert_eq!(ev_id(chosen), rawev_id(event2)); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_prefer_to_cache_nothing_rather_than_irrelevant_events() { - let event1 = make_event("m.room.power_levels", "$1"); - let events = &[event1]; - let chosen = choose_event_to_cache(events).await; - assert!(chosen.is_none()); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_cache_encrypted_events_that_are_after_latest_message() { - // Given two message events followed by two encrypted - let event1 = make_event("m.room.message", "$1"); - let event2 = make_event("m.room.message", "$2"); - let event3 = make_encrypted_event("$3"); - let event4 = make_encrypted_event("$4"); - let events = &[event1, event2.clone(), event3.clone(), event4.clone()]; - - // When I ask to cache events - let room = make_room(); - let mut room_info = room.clone_info(); - cache_latest_events(&room, &mut room_info, events, None, None).await; - - // The latest message is stored - assert_eq!( - ev_id(room_info.latest_event.as_ref().map(|latest_event| latest_event.event().clone())), - rawev_id(event2.clone()) - ); - - room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); - assert_eq!( - ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())), - rawev_id(event2) - ); - - // And also the two encrypted ones - assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event4])); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_dont_cache_encrypted_events_that_are_before_latest_message() { - // Given an encrypted event before and after the message - let event1 = make_encrypted_event("$1"); - let event2 = make_event("m.room.message", "$2"); - let event3 = make_encrypted_event("$3"); - let events = &[event1, event2.clone(), event3.clone()]; - - // When I ask to cache events - let room = make_room(); - let mut room_info = room.clone_info(); - cache_latest_events(&room, &mut room_info, events, None, None).await; - room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); - - // The latest message is stored - assert_eq!( - ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())), - rawev_id(event2) - ); - - // And also the encrypted one that was after it, but not the one before - assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3])); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_skip_irrelevant_events_eg_receipts_even_if_after_message() { - // Given two message events followed by two encrypted, with a receipt in the - // middle - let event1 = make_event("m.room.message", "$1"); - let event2 = make_event("m.room.message", "$2"); - let event3 = make_encrypted_event("$3"); - let event4 = make_event("m.read", "$4"); - let event5 = make_encrypted_event("$5"); - let events = &[event1, event2.clone(), event3.clone(), event4, event5.clone()]; - - // When I ask to cache events - let room = make_room(); - let mut room_info = room.clone_info(); - cache_latest_events(&room, &mut room_info, events, None, None).await; - room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); - - // The latest message is stored, ignoring the receipt - assert_eq!( - ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())), - rawev_id(event2) - ); - - // The two encrypted ones are stored, but not the receipt - assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event5])); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_only_store_the_max_number_of_encrypted_events() { - // Given two message events followed by lots of encrypted and other irrelevant - // events - let evente = make_event("m.room.message", "$e"); - let eventd = make_event("m.room.message", "$d"); - let eventc = make_encrypted_event("$c"); - let event9 = make_encrypted_event("$9"); - let event8 = make_encrypted_event("$8"); - let event7 = make_encrypted_event("$7"); - let eventb = make_event("m.read", "$b"); - let event6 = make_encrypted_event("$6"); - let event5 = make_encrypted_event("$5"); - let event4 = make_encrypted_event("$4"); - let event3 = make_encrypted_event("$3"); - let event2 = make_encrypted_event("$2"); - let eventa = make_event("m.read", "$a"); - let event1 = make_encrypted_event("$1"); - let event0 = make_encrypted_event("$0"); - let events = &[ - evente, - eventd.clone(), - eventc, - event9.clone(), - event8.clone(), - event7.clone(), - eventb, - event6.clone(), - event5.clone(), - event4.clone(), - event3.clone(), - event2.clone(), - eventa, - event1.clone(), - event0.clone(), - ]; - - // When I ask to cache events - let room = make_room(); - let mut room_info = room.clone_info(); - cache_latest_events(&room, &mut room_info, events, None, None).await; - room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); - - // The latest message is stored, ignoring encrypted and receipts - assert_eq!( - ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())), - rawev_id(eventd) - ); - - // Only 10 encrypted are stored, even though there were more - assert_eq!( - rawevs_ids(&room.latest_encrypted_events), - evs_ids(&[ - event9, event8, event7, event6, event5, event4, event3, event2, event1, event0 - ]) - ); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_dont_overflow_capacity_if_previous_encrypted_events_exist() { - // Given a RoomInfo with lots of encrypted events already inside it - let room = make_room(); - let mut room_info = room.clone_info(); - cache_latest_events( - &room, - &mut room_info, - &[ - make_encrypted_event("$0"), - make_encrypted_event("$1"), - make_encrypted_event("$2"), - make_encrypted_event("$3"), - make_encrypted_event("$4"), - make_encrypted_event("$5"), - make_encrypted_event("$6"), - make_encrypted_event("$7"), - make_encrypted_event("$8"), - make_encrypted_event("$9"), - ], - None, - None, - ) - .await; - room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); - - // Sanity: room_info has 10 encrypted events inside it - assert_eq!(room.latest_encrypted_events.read().unwrap().len(), 10); - - // When I ask to cache more encrypted events - let eventa = make_encrypted_event("$a"); - let mut room_info = room.clone_info(); - cache_latest_events(&room, &mut room_info, &[eventa], None, None).await; - room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); - - // The oldest event is gone - assert!(!rawevs_ids(&room.latest_encrypted_events).contains(&"$0".to_owned())); - - // The newest event is last in the list - assert_eq!(rawevs_ids(&room.latest_encrypted_events)[9], "$a"); - } - - #[cfg(feature = "e2e-encryption")] - #[async_test] - async fn test_existing_encrypted_events_are_deleted_if_we_receive_unencrypted() { - // Given a RoomInfo with some encrypted events already inside it - let room = make_room(); - let mut room_info = room.clone_info(); - cache_latest_events( - &room, - &mut room_info, - &[make_encrypted_event("$0"), make_encrypted_event("$1"), make_encrypted_event("$2")], - None, - None, - ) - .await; - room.set_room_info(room_info.clone(), RoomInfoNotableUpdateReasons::empty()); - - // When I ask to cache an unencrypted event, and some more encrypted events - let eventa = make_event("m.room.message", "$a"); - let eventb = make_encrypted_event("$b"); - cache_latest_events(&room, &mut room_info, &[eventa, eventb], None, None).await; - room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); - - // The only encrypted events stored are the ones after the decrypted one - assert_eq!(rawevs_ids(&room.latest_encrypted_events), &["$b"]); - - // The decrypted one is stored as the latest - assert_eq!(rawev_id(room.latest_event().unwrap().event().clone()), "$a"); - } - #[async_test] async fn test_recency_stamp_is_found_when_processing_sliding_sync_response() { // Given a logged-in client @@ -2504,93 +1979,6 @@ mod tests { ); } - #[cfg(feature = "e2e-encryption")] - async fn choose_event_to_cache(events: &[TimelineEvent]) -> Option { - let room = make_room(); - let mut room_info = room.clone_info(); - cache_latest_events(&room, &mut room_info, events, None, None).await; - room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty()); - room.latest_event().map(|latest_event| latest_event.event().clone()) - } - - #[cfg(feature = "e2e-encryption")] - fn rawev_id(event: TimelineEvent) -> String { - event.event_id().unwrap().to_string() - } - - fn ev_id(event: Option) -> String { - event.unwrap().event_id().unwrap().to_string() - } - - #[cfg(feature = "e2e-encryption")] - fn rawevs_ids(events: &Arc>>>) -> Vec { - events.read().unwrap().iter().map(|e| e.get_field("event_id").unwrap().unwrap()).collect() - } - - #[cfg(feature = "e2e-encryption")] - fn evs_ids(events: &[TimelineEvent]) -> Vec { - events.iter().map(|e| e.event_id().unwrap().to_string()).collect() - } - - #[cfg(feature = "e2e-encryption")] - fn make_room() -> Room { - let (sender, _receiver) = tokio::sync::broadcast::channel(1); - - Room::new( - user_id!("@u:e.co"), - Arc::new(MemoryStore::new()), - room_id!("!r:e.co"), - RoomState::Joined, - sender, - ) - } - - fn make_raw_event(event_type: &str, id: &str) -> Raw { - Raw::from_json_string( - json!({ - "type": event_type, - "event_id": id, - "content": { "msgtype": "m.text", "body": "my msg" }, - "sender": "@u:h.uk", - "origin_server_ts": 12344445, - }) - .to_string(), - ) - .unwrap() - } - - #[cfg(feature = "e2e-encryption")] - fn make_event(event_type: &str, id: &str) -> TimelineEvent { - TimelineEvent::from_plaintext(make_raw_event(event_type, id)) - } - - #[cfg(feature = "e2e-encryption")] - fn make_encrypted_event(id: &str) -> TimelineEvent { - TimelineEvent::from_utd( - Raw::from_json_string( - json!({ - "type": "m.room.encrypted", - "event_id": id, - "content": { - "algorithm": "m.megolm.v1.aes-sha2", - "ciphertext": "", - "sender_key": "", - "device_id": "", - "session_id": "", - }, - "sender": "@u:h.uk", - "origin_server_ts": 12344445, - }) - .to_string(), - ) - .unwrap(), - UnableToDecryptInfo { - session_id: Some("".to_owned()), - reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, - }, - ) - } - async fn membership( client: &BaseClient, room_id: &RoomId, @@ -2722,17 +2110,6 @@ mod tests { room } - fn room_with_timeline(events: &[serde_json::Value]) -> http::response::Room { - let mut room = http::response::Room::new(); - room.timeline.extend( - events - .iter() - .map(|e| Raw::from_json_string(e.to_string()).unwrap()) - .collect::>(), - ); - room - } - fn set_room_name(room: &mut http::response::Room, sender: &UserId, name: String) { room.required_state.push(make_state_event( sender, diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index 681a360a8dd..21c833f9fca 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -45,7 +45,7 @@ use serde::{Deserialize, Serialize}; use crate::{ MinimalStateEvent, OriginalMinimalStateEvent, RoomInfo, RoomState, deserialized_responses::SyncOrStrippedState, - latest_event::{LatestEvent, LatestEventValue}, + latest_event::LatestEventValue, room::{BaseRoomInfo, RoomSummary, SyncInfo}, sync::UnreadNotificationsCount, }; @@ -101,7 +101,7 @@ impl RoomInfoV1 { last_prev_batch, sync_info, encryption_state_synced, - latest_event, + latest_event: _, // deprecated base_info, } = self; @@ -115,7 +115,6 @@ impl RoomInfoV1 { last_prev_batch, sync_info, encryption_state_synced, - latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))), new_latest_event: LatestEventValue::None, read_receipts: Default::default(), base_info: base_info.migrate(create), diff --git a/testing/matrix-sdk-integration-testing/src/helpers.rs b/testing/matrix-sdk-integration-testing/src/helpers.rs index 39aeac1b28d..85019b16a97 100644 --- a/testing/matrix-sdk-integration-testing/src/helpers.rs +++ b/testing/matrix-sdk-integration-testing/src/helpers.rs @@ -87,11 +87,6 @@ impl TestClientBuilder { self } - pub fn http_proxy(mut self, url: String) -> Self { - self.http_proxy = Some(url); - self - } - pub fn cross_process_store_locks_holder_name(mut self, holder_name: String) -> Self { self.cross_process_store_locks_holder_name = Some(holder_name); self diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 6fde9316dc0..9a87fddea83 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -498,12 +498,10 @@ async fn test_room_notification_count() -> Result<()> { update_observer.next().await.expect("we should get an update when Bob joins the room"); assert_eq!(update.joined_members_count(), 2); - assert!(update.latest_event().is_none()); assert_eq!(alice_room.num_unread_messages(), 0); assert_eq!(alice_room.num_unread_mentions(), 0); assert_eq!(alice_room.num_unread_notifications(), 0); - assert!(alice_room.latest_event().is_none()); update_observer.assert_is_pending(); } @@ -519,8 +517,6 @@ async fn test_room_notification_count() -> Result<()> { .await .expect("we should get an update when Bob sent a non-mention message"); - assert!(update.latest_event().is_some()); - assert_eq!(alice_room.num_unread_messages(), 1); assert_eq!(alice_room.num_unread_notifications(), 1); assert_eq!(alice_room.num_unread_mentions(), 0); @@ -730,113 +726,6 @@ impl wiremock::Respond for &CustomResponder { } } -#[tokio::test] -#[ignore] -async fn test_delayed_decryption_latest_event() -> Result<()> { - let server = MockServer::start().await; - - // Setup mockserver that can drop to-device messages. - static CUSTOM_RESPONDER: Lazy> = - Lazy::new(|| Arc::new(CustomResponder::new())); - - server.register(Mock::given(AnyMatcher).respond_with(&**CUSTOM_RESPONDER)).await; - - let alice = - TestClientBuilder::new("alice").use_sqlite().http_proxy(server.uri()).build().await?; - let bob = TestClientBuilder::new("bob").use_sqlite().build().await?; - - let alice_sync_service = SyncService::builder(alice.clone()).build().await.unwrap(); - alice_sync_service.start().await; - - let bob_sync_service = SyncService::builder(bob.clone()).build().await.unwrap(); - bob_sync_service.start().await; - - // Alice creates a room and invites Bob. - let room = alice - .create_room(assign!(CreateRoomRequest::new(), { - invite: vec![bob.user_id().unwrap().to_owned()], - is_direct: true, - preset: Some(RoomPreset::TrustedPrivateChat), - })) - .await?; - - // Room is created by Alice. Let's enable encryption. - room.enable_encryption().await.unwrap(); - - // Wait for Alice to see its new room… - let alice_room = wait_for_room(&alice, room.room_id()).await; - - // …and for Bob to receive the invitation. - let bob_room = wait_for_room(&bob, room.room_id()).await; - - // Bob accepts the invitation by joining the room. - bob_room.join().await.unwrap(); - - assert_eq!(alice_room.state(), RoomState::Joined); - assert!(alice_room.latest_encryption_state().await.unwrap().is_encrypted()); - - assert_eq!(bob_room.state(), RoomState::Joined); - assert!(bob_room.latest_encryption_state().await.unwrap().is_encrypted()); - - // Get the room list of Alice. - let alice_all_rooms = alice_sync_service.room_list_service().all_rooms().await.unwrap(); - let (alice_room_list_stream, entries) = alice_all_rooms.entries_with_dynamic_adapters(10); - entries.set_filter(Box::new(new_filter_all(vec![]))); - pin_mut!(alice_room_list_stream); - - // The room list receives its initial rooms. - assert_let!( - Ok(Some(diffs)) = timeout(Duration::from_secs(5), alice_room_list_stream.next()).await - ); - assert_eq!(diffs.len(), 1); - assert_matches!( - &diffs[0], - VectorDiff::Reset { values: rooms } => { - assert_eq!(rooms.len(), 1, "{rooms:?}"); - assert_eq!(rooms[0].room_id(), room.room_id()); - } - ); - - // Send a message, but the keys won't arrive because to-device events are - // stripped away from the server's response - let event = bob_room.send(RoomMessageEventContent::text_plain("hello world")).await?; - - // Wait the message to be received by Alice. - assert_let!( - Ok(Some(diffs)) = timeout(Duration::from_secs(5), alice_room_list_stream.next()).await - ); - assert_eq!(diffs.len(), 1); - assert_matches!( - &diffs[0], - VectorDiff::Set { index: 0, value: room } => { - // The latest event is not decrypted. - assert!(room.latest_event().is_none()); - } - ); - - assert_pending!(alice_room_list_stream); - - // Now we allow the key to come through - *CUSTOM_RESPONDER.drop_todevice.lock().unwrap() = false; - - assert_let!( - Ok(Some(diffs)) = timeout(Duration::from_secs(5), alice_room_list_stream.next()).await - ); - assert_eq!(diffs.len(), 1); - assert_matches!( - &diffs[0], - VectorDiff::Set { index: 0, value: room } => { - // The latest event is now decrypted! - assert_eq!(room.latest_event().unwrap().event().event_id().unwrap(), event.event_id); - } - ); - - sleep(Duration::from_secs(2)).await; - assert_pending!(alice_room_list_stream); - - Ok(()) -} - async fn get_or_wait_for_room(client: &Client, room_id: &RoomId) -> Room { let (mut rooms, mut room_stream) = client.rooms_stream(); @@ -1009,9 +898,6 @@ async fn test_room_info_notable_update_deduplication() -> Result<()> { // Wait the message from Bob to be sent. sleep(Duration::from_secs(2)).await; - // Latest event is set now. - assert_eq!(alice_room.latest_event().unwrap().event_id(), Some(event.event_id)); - // Room has been updated because of the latest event. assert_let!(Ok(Some(diffs)) = timeout(Duration::from_secs(3), alice_rooms.next()).await); assert_eq!(diffs.len(), 1); From 31e3911c3d1d6d14b7d4556ee45777fba14bbc66 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 3 Sep 2025 17:17:04 +0200 Subject: [PATCH 4/4] refactor: Rename `new_latest_event` to `latest_event`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch removes the β€œnew_” prefix to the latest event API. --- bindings/matrix-sdk-ffi/src/room/mod.rs | 4 +-- .../matrix-sdk-base/src/room/latest_event.rs | 4 +-- crates/matrix-sdk-base/src/room/room_info.rs | 33 ++++++++----------- .../src/store/migration_helpers.rs | 2 +- crates/matrix-sdk-ui/src/timeline/traits.rs | 6 ++-- .../tests/integration/room_list_service.rs | 6 ++-- .../src/latest_events/latest_event.rs | 8 ++--- 7 files changed, 29 insertions(+), 34 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room/mod.rs b/bindings/matrix-sdk-ffi/src/room/mod.rs index 5f219f87c53..8c331675c3e 100644 --- a/bindings/matrix-sdk-ffi/src/room/mod.rs +++ b/bindings/matrix-sdk-ffi/src/room/mod.rs @@ -300,8 +300,8 @@ impl Room { .unwrap_or(false) } - async fn new_latest_event(&self) -> LatestEventValue { - self.inner.new_latest_event().await.into() + async fn latest_event(&self) -> LatestEventValue { + self.inner.latest_event().await.into() } pub async fn latest_encryption_state(&self) -> Result { diff --git a/crates/matrix-sdk-base/src/room/latest_event.rs b/crates/matrix-sdk-base/src/room/latest_event.rs index e1101a74430..8467444575c 100644 --- a/crates/matrix-sdk-base/src/room/latest_event.rs +++ b/crates/matrix-sdk-base/src/room/latest_event.rs @@ -17,7 +17,7 @@ use crate::latest_event::LatestEventValue; impl Room { /// Return the [`LatestEventValue`] of this room. - pub fn new_latest_event(&self) -> LatestEventValue { - self.inner.read().new_latest_event.clone() + pub fn latest_event(&self) -> LatestEventValue { + self.inner.read().latest_event_value.clone() } } diff --git a/crates/matrix-sdk-base/src/room/room_info.rs b/crates/matrix-sdk-base/src/room/room_info.rs index 4f4b35186df..a581e77d721 100644 --- a/crates/matrix-sdk-base/src/room/room_info.rs +++ b/crates/matrix-sdk-base/src/room/room_info.rs @@ -451,10 +451,11 @@ pub struct RoomInfo { pub(crate) encryption_state_synced: bool, /// The latest event value of this room. - /// - /// TODO(@hywan): Rename to `latest_event`. + // + // Note: Why `latest_event_value` and `latest_event`? Because `latest_event` existed before, we + // don't want to collide. #[serde(default)] - pub(crate) new_latest_event: LatestEventValue, + pub(crate) latest_event_value: LatestEventValue, /// Information about read receipts for this room. #[serde(default)] @@ -512,7 +513,7 @@ impl RoomInfo { last_prev_batch: None, sync_info: SyncInfo::NoState, encryption_state_synced: false, - new_latest_event: LatestEventValue::default(), + latest_event_value: LatestEventValue::default(), read_receipts: Default::default(), base_info: Box::new(BaseRoomInfo::new()), warned_about_unknown_room_version_rules: Arc::new(false.into()), @@ -1011,14 +1012,14 @@ impl RoomInfo { .collect() } - /// Return the new [`LatestEventValue`]. - pub fn new_latest_event(&self) -> &LatestEventValue { - &self.new_latest_event + /// Return the [`LatestEventValue`]. + pub fn latest_event(&self) -> &LatestEventValue { + &self.latest_event_value } - /// Sets the new [`LatestEventValue`]. - pub fn set_new_latest_event(&mut self, new_value: LatestEventValue) { - self.new_latest_event = new_value; + /// Set the [`LatestEventValue`]. + pub fn set_latest_event(&mut self, new_value: LatestEventValue) { + self.latest_event_value = new_value; } /// Updates the recency stamp of this room. @@ -1273,7 +1274,7 @@ mod tests { last_prev_batch: Some("pb".to_owned()), sync_info: SyncInfo::FullySynced, encryption_state_synced: true, - new_latest_event: LatestEventValue::None, + latest_event_value: LatestEventValue::None, base_info: Box::new( assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }), ), @@ -1306,13 +1307,7 @@ mod tests { "last_prev_batch": "pb", "sync_info": "FullySynced", "encryption_state_synced": true, - "latest_event": { - "event": { - "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}}, - "thread_summary": "None" - }, - }, - "new_latest_event": "None", + "latest_event_value": "None", "base_info": { "avatar": null, "canonical_alias": null, @@ -1507,7 +1502,7 @@ mod tests { assert_eq!(info.last_prev_batch, Some("pb".to_owned())); assert_eq!(info.sync_info, SyncInfo::FullySynced); assert!(info.encryption_state_synced); - assert_matches!(info.new_latest_event, LatestEventValue::None); + assert_matches!(info.latest_event_value, LatestEventValue::None); assert!(info.base_info.avatar.is_none()); assert!(info.base_info.canonical_alias.is_none()); assert!(info.base_info.create.is_none()); diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index 21c833f9fca..f5eff509c03 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -115,7 +115,7 @@ impl RoomInfoV1 { last_prev_batch, sync_info, encryption_state_synced, - new_latest_event: LatestEventValue::None, + latest_event_value: LatestEventValue::None, read_receipts: Default::default(), base_info: base_info.migrate(create), warned_about_unknown_room_version_rules: Arc::new(false.into()), diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index 25311fcc9cf..d6f3732c820 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -64,7 +64,7 @@ pub trait RoomExt { fn timeline_builder(&self) -> TimelineBuilder; /// Return a [`LatestEventValue`] corresponding to this room's latest event. - fn new_latest_event(&self) -> impl Future; + fn latest_event(&self) -> impl Future; } impl RoomExt for Room { @@ -76,9 +76,9 @@ impl RoomExt for Room { TimelineBuilder::new(self).track_read_marker_and_receipts() } - async fn new_latest_event(&self) -> LatestEventValue { + async fn latest_event(&self) -> LatestEventValue { LatestEventValue::from_base_latest_event_value( - (**self).new_latest_event(), + (**self).latest_event(), self, &self.client(), ) diff --git a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs index 0bc0f14adce..e8e92502992 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -2584,7 +2584,7 @@ async fn test_room_latest_event() -> Result<(), Error> { latest_events.listen_to_room(room_id).await.unwrap(); // The latest event does not exist. - assert_matches!(room.new_latest_event().await, LatestEventValue::None); + assert_matches!(room.latest_event().await, LatestEventValue::None); sync_then_assert_request_and_fake_response! { [server, room_list, sync] @@ -2606,7 +2606,7 @@ async fn test_room_latest_event() -> Result<(), Error> { yield_now().await; // The latest event exists. - assert_matches!(room.new_latest_event().await, LatestEventValue::Remote { .. }); + assert_matches!(room.latest_event().await, LatestEventValue::Remote { .. }); // Insert a local event in the `Timeline`. timeline.send(RoomMessageEventContent::text_plain("Hello, World!").into()).await.unwrap(); @@ -2615,7 +2615,7 @@ async fn test_room_latest_event() -> Result<(), Error> { yield_now().await; // The latest event has been updated. - assert_matches!(room.new_latest_event().await, LatestEventValue::Local { .. }); + assert_matches!(room.latest_event().await, LatestEventValue::Local { .. }); Ok(()) } diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index e9afe6b3653..5d66ab8e159 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -146,7 +146,7 @@ impl LatestEvent { // Compute a new `RoomInfo`. let mut room_info = room.clone_info(); - room_info.set_new_latest_event(new_value); + room_info.set_latest_event(new_value); let mut state_changes = StateChanges::default(); state_changes.add_room(room_info.clone()); @@ -426,7 +426,7 @@ mod tests_latest_event { // Check there is no `LatestEventValue` for the moment. { - let latest_event = room.new_latest_event(); + let latest_event = room.latest_event(); assert_matches!(latest_event, LatestEventValue::None); } @@ -452,7 +452,7 @@ mod tests_latest_event { // Check it's in the `RoomInfo` and in `Room`. { - let latest_event = room.new_latest_event(); + let latest_event = room.latest_event(); assert_matches!(latest_event, LatestEventValue::Remote(_)); } @@ -467,7 +467,7 @@ mod tests_latest_event { .build() .await; let room = client.get_room(&room_id).unwrap(); - let latest_event = room.new_latest_event(); + let latest_event = room.latest_event(); assert_matches!(latest_event, LatestEventValue::Remote(_)); }