diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 51f864a47df..086ba265155 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -758,6 +758,7 @@ impl BaseClient { room: &Room, ) -> Option<(Box, usize)> { let enc_events = room.latest_encrypted_events(); + let prev_latest = room.latest_event().and_then(|latest| latest.event_id()); // Walk backwards through the encrypted events, looking for one we can decrypt for (i, event) in enc_events.iter().enumerate().rev() { @@ -771,7 +772,7 @@ impl BaseClient { // We found an event we can decrypt if let Ok(any_sync_event) = decrypted.event.deserialize() { // We can deserialize it to find its type - match is_suitable_for_latest_event(&any_sync_event) { + match is_suitable_for_latest_event(&any_sync_event, prev_latest.as_deref()) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 6904fe811f4..cd7294bdccb 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -8,10 +8,11 @@ use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use ruma::events::{ call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent}, poll::unstable_start::SyncUnstablePollStartEvent, - relation::RelationType, room::message::SyncRoomMessageEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, }; +#[cfg(feature = "e2e-encryption")] +use ruma::EventId; use ruma::{events::sticker::SyncStickerEvent, MxcUri, OwnedEventId}; use serde::{Deserialize, Serialize}; @@ -50,26 +51,33 @@ pub enum PossibleLatestEvent<'a> { /// 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(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> { +pub fn is_suitable_for_latest_event<'a>( + event: &'a AnySyncTimelineEvent, + prev_latest: Option<&EventId>, +) -> PossibleLatestEvent<'a> { + use ruma::events::room::message::Relation; + match event { // Suitable - we have an m.room.message that was not redacted or edited AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => { // Check if this is a replacement for another message. If it is, ignore it if let Some(original_message) = message.as_original() { - let is_replacement = - original_message.content.relates_to.as_ref().map_or(false, |relates_to| { - if let Some(relation_type) = relates_to.rel_type() { - relation_type == RelationType::Replacement + // We include messages that are not replacements; but we allow a replacement if + // that's the replacement to the previous known latest event. + let is_suitable = + original_message.content.relates_to.as_ref().map_or(true, |relates_to| { + if let Relation::Replacement(c) = &relates_to { + prev_latest == Some(&c.event_id) } else { false } }); - if is_replacement { + if !is_suitable { return PossibleLatestEvent::NoUnsupportedMessageLikeType; - } else { - return PossibleLatestEvent::YesRoomMessage(message); } + + return PossibleLatestEvent::YesRoomMessage(message); } return PossibleLatestEvent::YesRoomMessage(message); @@ -328,7 +336,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); assert_eq!(m.content.msgtype.msgtype(), "m.image"); @@ -351,7 +359,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); assert_eq!(m.content.poll_start().question.text, "do you like rust?"); @@ -375,7 +383,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); } @@ -397,7 +405,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); } @@ -418,7 +426,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_)) ); } @@ -440,7 +448,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedMessageLikeType ); } @@ -468,7 +476,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_)) ); } @@ -490,7 +498,10 @@ mod tests { }), )); - assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted); + assert_matches!( + is_suitable_for_latest_event(&event, None), + PossibleLatestEvent::NoEncrypted + ); } #[test] @@ -507,7 +518,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedEventType ); } @@ -531,7 +542,63 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), + PossibleLatestEvent::NoUnsupportedMessageLikeType + ); + } + + #[test] + fn test_replacement_events_is_suitable_if_replaces_previous_latest_event() { + let prev_latest_event_id = owned_event_id!("$1"); + + // The content's body is the fallback for the edit. + let mut event_content = RoomMessageEventContent::text_plain("*Bye bye, world!"); + + // This is the actual edit. + event_content.relates_to = Some(Relation::Replacement(Replacement::new( + prev_latest_event_id.clone(), + RoomMessageEventContent::text_plain("Bye bye, 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_let!( + PossibleLatestEvent::YesRoomMessage(sync_message_event) = + is_suitable_for_latest_event(&event, Some(&prev_latest_event_id)) + ); + assert_eq!(sync_message_event.as_original().unwrap().content.body(), "*Bye bye, world!"); + } + + #[test] + fn test_replacement_events_is_suitable_if_doesnt_replace_previous_latest_event() { + let prev_latest_event_id = owned_event_id!("$42"); + + 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, Some(&prev_latest_event_id)), PossibleLatestEvent::NoUnsupportedMessageLikeType ); } diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 05a9e9a3521..90371b56d10 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -669,9 +669,11 @@ async fn cache_latest_events( let mut encrypted_events = Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); + let prev_latest = room_info.latest_event.as_ref().and_then(|latest| latest.event_id()); + for event in events.iter().rev() { if let Ok(timeline_event) = event.event.deserialize() { - match is_suitable_for_latest_event(&timeline_event) { + match is_suitable_for_latest_event(&timeline_event, prev_latest.as_deref()) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) 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 d36c33324b1..34861480c2c 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 @@ -17,7 +17,6 @@ use std::sync::Arc; use as_variant::as_variant; use imbl::Vector; use matrix_sdk::crypto::types::events::UtdCause; -use matrix_sdk_base::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}; use ruma::{ events::{ call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent}, @@ -50,10 +49,11 @@ use ruma::{ sticker::{StickerEventContent, SyncStickerEvent}, AnyFullStateEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, BundledMessageLikeRelations, FullStateEventContent, MessageLikeEventType, StateEventType, + SyncMessageLikeEvent, }, OwnedDeviceId, OwnedMxcUri, OwnedUserId, RoomVersionId, UserId, }; -use tracing::warn; +use tracing::{debug, warn}; use crate::timeline::{polls::PollState, TimelineItem}; @@ -123,39 +123,37 @@ impl TimelineItemContent { pub(crate) fn from_latest_event_content( event: AnySyncTimelineEvent, ) -> Option { - match is_suitable_for_latest_event(&event) { - 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 + match event { + AnySyncTimelineEvent::MessageLike(msg_like) => { + let bundled_relations = msg_like.relations(); + match msg_like { + AnySyncMessageLikeEvent::RoomMessage(room_msg) => { + Some(Self::from_suitable_latest_event_content(room_msg, bundled_relations)) + } + AnySyncMessageLikeEvent::Sticker(sticker) => { + Some(Self::from_suitable_latest_sticker_content(sticker)) + } + AnySyncMessageLikeEvent::UnstablePollStart(poll_start) => { + Some(Self::from_suitable_latest_poll_event_content(&poll_start)) + } + AnySyncMessageLikeEvent::CallInvite(call_invite) => { + Some(Self::from_suitable_latest_call_invite_content(&call_invite)) + } + AnySyncMessageLikeEvent::CallNotify(call_notify) => { + Some(Self::from_suitable_latest_call_notify_content(&call_notify)) + } + _ => { + warn!( + "Found latest event of type {}, but don't know how to wrap it.", + msg_like.event_type() + ); + None + } + } } - PossibleLatestEvent::NoEncrypted => { - warn!("Found an encrypted event cached as latest_event! ID={}", event.event_id()); + + AnySyncTimelineEvent::State(_) => { + debug!("State events as latest events not supported yet"); None } } @@ -164,45 +162,33 @@ impl TimelineItemContent { /// 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(); - - // We don't have access to any relations via the `AnySyncTimelineEvent` (I think - // - andyb) so we pretend there are none. This might be OK for - // the message preview use case. - let relations = BundledMessageLikeRelations::new(); - - // If this message is a reply, we would look up in this list the message it was - // replying to. Since we probably won't show this in the message preview, - // it's probably OK to supply an empty list here. - // `Message::from_event` marks the original event as `Unavailable` if it can't - // be found inside the timeline_items. - let timeline_items = Vector::new(); - TimelineItemContent::Message(Message::from_event( - event_content, - relations, - &timeline_items, - )) - } - SyncRoomMessageEvent::Redacted(_) => TimelineItemContent::RedactedMessage, - } + fn from_suitable_latest_event_content( + event: SyncRoomMessageEvent, + relations: BundledMessageLikeRelations, + ) -> TimelineItemContent { + let SyncMessageLikeEvent::Original(event) = event else { + return TimelineItemContent::RedactedMessage; + }; + + // If this message is a reply, we would look up in this list the message it was + // replying to. Since we probably won't show this in the message preview, + // it's probably OK to supply an empty list here. + // + // `Message::from_event` marks the original event as `Unavailable` if it can't + // be found inside the timeline_items. + let timeline_items = Vector::new(); + + TimelineItemContent::Message(Message::from_event(event.content, relations, &timeline_items)) } /// 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(); - TimelineItemContent::Sticker(Sticker { content: event_content }) - } - SyncStickerEvent::Redacted(_) => TimelineItemContent::RedactedMessage, - } + fn from_suitable_latest_sticker_content(event: SyncStickerEvent) -> TimelineItemContent { + let SyncMessageLikeEvent::Original(event) = event else { + return TimelineItemContent::RedactedMessage; + }; + TimelineItemContent::Sticker(Sticker { content: event.content }) } /// Extracts a `TimelineItemContent` from a poll start event for use as a @@ -210,14 +196,12 @@ impl TimelineItemContent { fn from_suitable_latest_poll_event_content( event: &SyncUnstablePollStartEvent, ) -> TimelineItemContent { - match event { - SyncUnstablePollStartEvent::Original(event) => { - TimelineItemContent::Poll(PollState::new(NewUnstablePollStartEventContent::new( - event.content.poll_start().clone(), - ))) - } - SyncUnstablePollStartEvent::Redacted(_) => TimelineItemContent::RedactedMessage, - } + let Some(event) = event.as_original() else { + return TimelineItemContent::RedactedMessage; + }; + TimelineItemContent::Poll(PollState::new(NewUnstablePollStartEventContent::new( + event.content.poll_start().clone(), + ))) } fn from_suitable_latest_call_invite_content( diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 86cd72533f3..a27b9318c6f 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -283,7 +283,8 @@ impl Timeline { /// Get the latest of the timeline's event items. pub async fn latest_event(&self) -> Option { if self.inner.is_live().await { - self.inner.items().await.last()?.as_event().cloned() + // Try to find the latest item that's an event (and skip virtual items). + self.inner.items().await.iter().rev().find_map(|item| item.as_event()).cloned() } else { None }