mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-07 15:33:45 -04:00
chore(ui): Remove the old latest event API.
So satisfying.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user