chore(ui): Remove the old latest event API.

So satisfying.
This commit is contained in:
Ivan Enderlin
2025-12-04 13:13:01 +01:00
parent e4141b216a
commit 91091c7819
7 changed files with 30 additions and 844 deletions

View File

@@ -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<TimelineItemContent> {
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)
}

View File

@@ -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<EventTimelineItem> {
// 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**", "<b>My M</b>")
.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, "<b>My M</b>");
} 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**", "<b>My M</b>")
.sender(user_id)
.event_id(original_event_id)
.with_bundled_edit(
f.text_html(" * Updated!", " * <b>Updated!</b>")
.edit(
original_event_id,
MessageType::text_html("Updated!", "<b>Updated!</b>").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, "<b>Updated!</b>");
} 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**", "<b>My M</b>")
.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**", "<b>My M</b>").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::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
.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<AnySyncStateEvent> {
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
}
}

View File

@@ -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<EventTimelineItem> {
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<OwnedEventId> {
self.controller.latest_event_id().await

View File

@@ -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<Profile> {
None
}
async fn load_user_receipt<'a>(
&'a self,
receipt_type: ReceiptType,

View File

@@ -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<Output = Option<EventTimelineItem>> + SendOutsideWasm;
/// Return a [`LatestEventValue`] corresponding to this room's latest event.
fn new_latest_event(&self) -> impl Future<Output = LatestEventValue>;
}
@@ -82,14 +74,6 @@ impl RoomExt for Room {
.track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
}
async fn latest_event_item(&self) -> Option<EventTimelineItem> {
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<Output = Option<Profile>> + SendOutsideWasm + 'a;
fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option<Profile>;
/// 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<Profile> {
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,

View File

@@ -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(())
}

View File

@@ -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())