From 91091c7819c242eb07f7c5fef2d7a5a3afc293a0 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 4 Dec 2025 13:13:01 +0100 Subject: [PATCH] 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 | 15 - .../matrix-sdk-ui/src/timeline/tests/mod.rs | 8 +- crates/matrix-sdk-ui/src/timeline/traits.rs | 33 +- .../tests/integration/room_list_service.rs | 66 +-- .../integration/timeline/read_receipts.rs | 4 +- 7 files changed, 30 insertions(+), 844 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 f913786b1..c86931664 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 @@ -15,21 +15,16 @@ use std::sync::Arc; use as_variant::as_variant; -use matrix_sdk_base::{ - crypto::types::events::UtdCause, - latest_event::{PossibleLatestEvent, is_suitable_for_latest_event}, -}; +use matrix_sdk_base::crypto::types::events::UtdCause; use ruma::{ OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedUserId, UserId, events::{ - AnyFullStateEventContent, AnySyncTimelineEvent, FullStateEventContent, Mentions, - MessageLikeEventType, StateEventType, - call::invite::SyncCallInviteEvent, + AnyFullStateEventContent, FullStateEventContent, Mentions, MessageLikeEventType, + StateEventType, policy::rule::{ room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent, user::PolicyRuleUserEventContent, }, - poll::unstable_start::{SyncUnstablePollStartEvent, UnstablePollStartEventContent}, room::{ aliases::RoomAliasesEventContent, avatar::RoomAvatarEventContent, @@ -40,24 +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, }, - rtc::notification::SyncRtcNotificationEvent, space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, - sticker::{StickerEventContent, SyncStickerEvent}, + sticker::StickerEventContent, }, html::RemoveReplyFallback, room_version_rules::RedactionRules, }; -use tracing::warn; mod message; mod msg_like; @@ -123,220 +116,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::YesRtcNotification(rtc_notification) => { - Some(Self::from_suitable_latest_rtc_notification_content(rtc_notification)) - } - 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(event.content.poll_start().clone(), None); - 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_rtc_notification_content( - event: &SyncRtcNotificationEvent, - ) -> TimelineItemContent { - match event { - SyncRtcNotificationEvent::Original(_) => TimelineItemContent::RtcNotification, - SyncRtcNotificationEvent::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 e73d79780..97595a4fb 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 @@ -787,416 +693,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.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 83f2c2c32..e4f8420b8 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -252,21 +252,6 @@ impl Timeline { Some(item.to_owned()) } - /// Get the latest of the timeline's event items, both remote and local. - pub async fn latest_event(&self) -> Option { - if self.controller.is_live() { - self.controller.items().await.iter().rev().find_map(|item| { - if let TimelineItemKind::Event(event) = item.kind() { - Some(event.to_owned()) - } else { - None - } - }) - } else { - None - } - } - /// Get the latest of the timeline's remote event ids. pub async fn latest_event_id(&self) -> Option { self.controller.latest_event_id().await diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 55411d2be..657d545e0 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -34,9 +34,7 @@ use matrix_sdk::{ room::{EventWithContextResponse, Messages, MessagesOptions, 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, OwnedTransactionId, OwnedUserId, @@ -347,10 +345,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 cd94a0ca7..b044a6b4d 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -21,9 +21,7 @@ use matrix_sdk::{ deserialized_responses::TimelineEvent, paginators::{PaginableRoom, thread::PaginableThread}, }; -use matrix_sdk_base::{ - RoomInfo, crypto::types::events::CryptoContextInfo, latest_event::LatestEvent, -}; +use matrix_sdk_base::{RoomInfo, crypto::types::events::CryptoContextInfo}; use ruma::{ EventId, OwnedEventId, OwnedTransactionId, OwnedUserId, UserId, events::{ @@ -35,7 +33,7 @@ use ruma::{ }; use tracing::error; -use super::{EventTimelineItem, Profile, RedactError, TimelineBuilder}; +use super::{Profile, RedactError, TimelineBuilder}; use crate::timeline::{ self, Timeline, TimelineReadReceiptTracking, latest_event::LatestEventValue, pinned_events_loader::PinnedEventsRoom, @@ -62,12 +60,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 +74,6 @@ impl RoomExt for Room { .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents) } - 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 +97,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>( @@ -185,18 +168,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 80944df10..7729fd08d 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::{collections::BTreeMap, ops::Not, sync::Arc}; +use std::{collections::BTreeMap, 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 as _, 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}, @@ -2609,7 +2608,7 @@ 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?; mock_encryption_state(&server, false).await; let sync = room_list.sync(); @@ -2638,8 +2637,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] @@ -2657,58 +2662,19 @@ async fn test_room_latest_event() -> Result<(), Error> { }, }; + yield_now().await; + // 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"))); - - // 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()); - } - ); + 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 051257e17..e6b97241e 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs @@ -1174,9 +1174,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())