1use std::{
16 ops::{Deref, DerefMut},
17 sync::Arc,
18};
19
20use as_variant::as_variant;
21use indexmap::IndexMap;
22use matrix_sdk::{
23 deserialized_responses::{EncryptionInfo, ShieldState},
24 send_queue::{SendHandle, SendReactionHandle},
25 Client, Error,
26};
27use matrix_sdk_base::{
28 deserialized_responses::{ShieldStateCode, SENT_IN_CLEAR},
29 latest_event::LatestEvent,
30};
31use once_cell::sync::Lazy;
32use ruma::{
33 events::{receipt::Receipt, room::message::MessageType, AnySyncTimelineEvent},
34 serde::Raw,
35 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
36 OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
37};
38use tracing::warn;
39use unicode_segmentation::UnicodeSegmentation;
40
41mod content;
42mod local;
43mod remote;
44
45pub(super) use self::{
46 content::{
47 extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
48 },
49 local::LocalEventTimelineItem,
50 remote::{RemoteEventOrigin, RemoteEventTimelineItem},
51};
52pub use self::{
53 content::{
54 AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange,
55 MembershipChange, Message, OtherState, PollResult, PollState, RepliedToEvent,
56 RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineItemContent,
57 },
58 local::EventSendState,
59};
60use super::{RepliedToInfo, ReplyContent, UnsupportedReplyItem};
61
62#[derive(Clone, Debug)]
68pub struct EventTimelineItem {
69 pub(super) sender: OwnedUserId,
71 pub(super) sender_profile: TimelineDetails<Profile>,
73 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
75 pub(super) content: TimelineItemContent,
77 pub(super) kind: EventTimelineItemKind,
79 pub(super) is_room_encrypted: bool,
83}
84
85#[derive(Clone, Debug)]
86pub(super) enum EventTimelineItemKind {
87 Local(LocalEventTimelineItem),
89 Remote(RemoteEventTimelineItem),
91}
92
93#[derive(Clone, Debug, Eq, Hash, PartialEq)]
95pub enum TimelineEventItemId {
96 TransactionId(OwnedTransactionId),
99 EventId(OwnedEventId),
101}
102
103pub(crate) enum TimelineItemHandle<'a> {
109 Remote(&'a EventId),
110 Local(&'a SendHandle),
111}
112
113impl EventTimelineItem {
114 pub(super) fn new(
115 sender: OwnedUserId,
116 sender_profile: TimelineDetails<Profile>,
117 timestamp: MilliSecondsSinceUnixEpoch,
118 content: TimelineItemContent,
119 kind: EventTimelineItemKind,
120 is_room_encrypted: bool,
121 ) -> Self {
122 Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted }
123 }
124
125 pub async fn from_latest_event(
137 client: Client,
138 room_id: &RoomId,
139 latest_event: LatestEvent,
140 ) -> Option<EventTimelineItem> {
141 use super::traits::RoomDataProvider;
145
146 let raw_sync_event = latest_event.event().raw().clone();
147 let encryption_info = latest_event.event().encryption_info().cloned();
148
149 let Ok(event) = raw_sync_event.deserialize_as::<AnySyncTimelineEvent>() else {
150 warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!");
151 return None;
152 };
153
154 let timestamp = event.origin_server_ts();
155 let sender = event.sender().to_owned();
156 let event_id = event.event_id().to_owned();
157 let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false);
158
159 let power_levels = if let Some(room) = client.get_room(room_id) {
161 room.power_levels().await.ok()
162 } else {
163 None
164 };
165 let room_power_levels_info = client.user_id().zip(power_levels.as_ref());
166
167 let content =
170 TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?;
171
172 let read_receipts = IndexMap::new();
174
175 let is_highlighted = false;
177
178 let latest_edit_json = None;
181
182 let origin = RemoteEventOrigin::Sync;
184
185 let kind = RemoteEventTimelineItem {
186 event_id,
187 transaction_id: None,
188 read_receipts,
189 is_own,
190 is_highlighted,
191 encryption_info,
192 original_json: Some(raw_sync_event),
193 latest_edit_json,
194 origin,
195 }
196 .into();
197
198 let room = client.get_room(room_id);
199 let sender_profile = if let Some(room) = room {
200 let mut profile = room.profile_from_latest_event(&latest_event);
201
202 if profile.is_none() {
204 profile = room.profile_from_user_id(&sender).await;
205 }
206
207 profile.map(TimelineDetails::Ready).unwrap_or(TimelineDetails::Unavailable)
208 } else {
209 TimelineDetails::Unavailable
210 };
211
212 Some(Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted: false })
213 }
214
215 pub fn is_local_echo(&self) -> bool {
222 matches!(self.kind, EventTimelineItemKind::Local(_))
223 }
224
225 pub fn is_remote_event(&self) -> bool {
233 matches!(self.kind, EventTimelineItemKind::Remote(_))
234 }
235
236 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
238 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
239 }
240
241 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
243 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
244 }
245
246 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
249 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
250 }
251
252 pub fn send_state(&self) -> Option<&EventSendState> {
254 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
255 }
256
257 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
259 match &self.kind {
260 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
261 EventTimelineItemKind::Remote(_) => None,
262 }
263 }
264
265 pub fn identifier(&self) -> TimelineEventItemId {
271 match &self.kind {
272 EventTimelineItemKind::Local(local) => local.identifier(),
273 EventTimelineItemKind::Remote(remote) => {
274 TimelineEventItemId::EventId(remote.event_id.clone())
275 }
276 }
277 }
278
279 pub fn transaction_id(&self) -> Option<&TransactionId> {
284 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
285 }
286
287 pub fn event_id(&self) -> Option<&EventId> {
296 match &self.kind {
297 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
298 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
299 }
300 }
301
302 pub fn sender(&self) -> &UserId {
304 &self.sender
305 }
306
307 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
309 &self.sender_profile
310 }
311
312 pub fn content(&self) -> &TimelineItemContent {
314 &self.content
315 }
316
317 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
324 static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
325 match &self.kind {
326 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
327 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
328 }
329 }
330
331 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
337 self.timestamp
338 }
339
340 pub fn is_own(&self) -> bool {
342 match &self.kind {
343 EventTimelineItemKind::Local(_) => true,
344 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
345 }
346 }
347
348 pub fn is_editable(&self) -> bool {
350 if !self.is_own() {
354 return false;
356 }
357
358 match self.content() {
359 TimelineItemContent::Message(message) => {
360 matches!(
361 message.msgtype(),
362 MessageType::Text(_)
363 | MessageType::Emote(_)
364 | MessageType::Audio(_)
365 | MessageType::File(_)
366 | MessageType::Image(_)
367 | MessageType::Video(_)
368 )
369 }
370 TimelineItemContent::Poll(poll) => {
371 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
372 }
373 _ => {
374 false
376 }
377 }
378 }
379
380 pub fn is_highlighted(&self) -> bool {
382 match &self.kind {
383 EventTimelineItemKind::Local(_) => false,
384 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
385 }
386 }
387
388 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
390 match &self.kind {
391 EventTimelineItemKind::Local(_) => None,
392 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_ref(),
393 }
394 }
395
396 pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
399 if !self.is_room_encrypted || self.is_local_echo() {
400 return None;
401 }
402
403 if let TimelineItemContent::UnableToDecrypt(_) = self.content() {
405 return None;
406 }
407
408 match self.encryption_info() {
409 Some(info) => {
410 if strict {
411 Some(info.verification_state.to_shield_state_strict())
412 } else {
413 Some(info.verification_state.to_shield_state_lax())
414 }
415 }
416 None => Some(ShieldState::Red {
417 code: ShieldStateCode::SentInClear,
418 message: SENT_IN_CLEAR,
419 }),
420 }
421 }
422
423 pub fn can_be_replied_to(&self) -> bool {
425 if self.event_id().is_none() {
427 false
428 } else if let TimelineItemContent::Message(_) = self.content() {
429 true
430 } else {
431 self.latest_json().is_some()
432 }
433 }
434
435 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
441 match &self.kind {
442 EventTimelineItemKind::Local(_) => None,
443 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
444 }
445 }
446
447 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
449 match &self.kind {
450 EventTimelineItemKind::Local(_) => None,
451 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
452 }
453 }
454
455 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
458 self.latest_edit_json().or_else(|| self.original_json())
459 }
460
461 pub fn origin(&self) -> Option<EventItemOrigin> {
465 match &self.kind {
466 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
467 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
468 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
469 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
470 _ => None,
471 },
472 }
473 }
474
475 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
476 self.content = content;
477 }
478
479 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
481 Self { kind: kind.into(), ..self.clone() }
482 }
483
484 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
486 let mut new = self.clone();
487 new.content = new_content;
488 new
489 }
490
491 pub(super) fn with_content_and_latest_edit(
496 &self,
497 new_content: TimelineItemContent,
498 edit_json: Option<Raw<AnySyncTimelineEvent>>,
499 ) -> Self {
500 let mut new = self.clone();
501 new.content = new_content;
502 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
503 r.latest_edit_json = edit_json;
504 }
505 new
506 }
507
508 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
510 Self { sender_profile, ..self.clone() }
511 }
512
513 pub(super) fn with_encryption_info(&self, encryption_info: Option<EncryptionInfo>) -> Self {
515 let mut new = self.clone();
516 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
517 r.encryption_info = encryption_info;
518 }
519
520 new
521 }
522
523 pub(super) fn redact(&self, room_version: &RoomVersionId) -> Self {
525 let content = self.content.redact(room_version);
526 let kind = match &self.kind {
527 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
528 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
529 };
530 Self {
531 sender: self.sender.clone(),
532 sender_profile: self.sender_profile.clone(),
533 timestamp: self.timestamp,
534 content,
535 kind,
536 is_room_encrypted: self.is_room_encrypted,
537 }
538 }
539
540 pub fn replied_to_info(&self) -> Result<RepliedToInfo, UnsupportedReplyItem> {
542 let reply_content = match self.content() {
543 TimelineItemContent::Message(msg) => ReplyContent::Message(msg.to_owned()),
544 _ => {
545 let Some(raw_event) = self.latest_json() else {
546 return Err(UnsupportedReplyItem::MissingJson);
547 };
548
549 ReplyContent::Raw(raw_event.clone())
550 }
551 };
552
553 let Some(event_id) = self.event_id() else {
554 return Err(UnsupportedReplyItem::MissingEventId);
555 };
556
557 Ok(RepliedToInfo {
558 event_id: event_id.to_owned(),
559 sender: self.sender().to_owned(),
560 timestamp: self.timestamp(),
561 content: reply_content,
562 })
563 }
564
565 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
566 match &self.kind {
567 EventTimelineItemKind::Local(local) => {
568 if let Some(event_id) = local.event_id() {
569 TimelineItemHandle::Remote(event_id)
570 } else {
571 TimelineItemHandle::Local(
572 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
574 )
575 }
576 }
577 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
578 }
579 }
580
581 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
583 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
584 }
585
586 pub fn contains_only_emojis(&self) -> bool {
609 let body = match self.content() {
610 TimelineItemContent::Message(msg) => match msg.msgtype() {
611 MessageType::Text(text) => Some(text.body.as_str()),
612 MessageType::Audio(audio) => audio.caption(),
613 MessageType::File(file) => file.caption(),
614 MessageType::Image(image) => image.caption(),
615 MessageType::Video(video) => video.caption(),
616 _ => None,
617 },
618 TimelineItemContent::RedactedMessage
619 | TimelineItemContent::Sticker(_)
620 | TimelineItemContent::UnableToDecrypt(_)
621 | TimelineItemContent::MembershipChange(_)
622 | TimelineItemContent::ProfileChange(_)
623 | TimelineItemContent::OtherState(_)
624 | TimelineItemContent::FailedToParseMessageLike { .. }
625 | TimelineItemContent::FailedToParseState { .. }
626 | TimelineItemContent::Poll(_)
627 | TimelineItemContent::CallInvite
628 | TimelineItemContent::CallNotify => None,
629 };
630
631 if let Some(body) = body {
632 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
634
635 if graphemes.len() > 5 {
640 return false;
641 }
642
643 graphemes.iter().all(|g| emojis::get(g).is_some())
644 } else {
645 false
646 }
647 }
648}
649
650impl From<LocalEventTimelineItem> for EventTimelineItemKind {
651 fn from(value: LocalEventTimelineItem) -> Self {
652 EventTimelineItemKind::Local(value)
653 }
654}
655
656impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
657 fn from(value: RemoteEventTimelineItem) -> Self {
658 EventTimelineItemKind::Remote(value)
659 }
660}
661
662#[derive(Clone, Debug, Default, PartialEq, Eq)]
664pub struct Profile {
665 pub display_name: Option<String>,
667
668 pub display_name_ambiguous: bool,
674
675 pub avatar_url: Option<OwnedMxcUri>,
677}
678
679#[derive(Clone, Debug)]
683pub enum TimelineDetails<T> {
684 Unavailable,
687
688 Pending,
690
691 Ready(T),
693
694 Error(Arc<Error>),
696}
697
698impl<T> TimelineDetails<T> {
699 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
700 match value {
701 Some(v) => Self::Ready(v),
702 None => Self::Unavailable,
703 }
704 }
705
706 pub(crate) fn is_unavailable(&self) -> bool {
707 matches!(self, Self::Unavailable)
708 }
709
710 pub fn is_ready(&self) -> bool {
711 matches!(self, Self::Ready(_))
712 }
713}
714
715#[derive(Clone, Copy, Debug)]
717#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
718pub enum EventItemOrigin {
719 Local,
721 Sync,
723 Pagination,
725}
726
727#[derive(Clone, Debug)]
729pub enum ReactionStatus {
730 LocalToLocal(Option<SendReactionHandle>),
734 LocalToRemote(Option<SendHandle>),
738 RemoteToRemote(OwnedEventId),
742}
743
744#[derive(Clone, Debug)]
746pub struct ReactionInfo {
747 pub timestamp: MilliSecondsSinceUnixEpoch,
748 pub status: ReactionStatus,
750}
751
752#[derive(Debug, Clone, Default)]
757pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
758
759impl Deref for ReactionsByKeyBySender {
760 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
761
762 fn deref(&self) -> &Self::Target {
763 &self.0
764 }
765}
766
767impl DerefMut for ReactionsByKeyBySender {
768 fn deref_mut(&mut self) -> &mut Self::Target {
769 &mut self.0
770 }
771}
772
773impl ReactionsByKeyBySender {
774 pub(crate) fn remove_reaction(
780 &mut self,
781 sender: &UserId,
782 annotation: &str,
783 ) -> Option<ReactionInfo> {
784 if let Some(by_user) = self.0.get_mut(annotation) {
785 if let Some(info) = by_user.swap_remove(sender) {
786 if by_user.is_empty() {
788 self.0.swap_remove(annotation);
789 }
790 return Some(info);
791 }
792 }
793 None
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use assert_matches::assert_matches;
800 use assert_matches2::assert_let;
801 use matrix_sdk::test_utils::logged_in_client;
802 use matrix_sdk_base::{
803 deserialized_responses::TimelineEvent, latest_event::LatestEvent, MinimalStateEvent,
804 OriginalMinimalStateEvent,
805 };
806 use matrix_sdk_test::{
807 async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event,
808 };
809 use ruma::{
810 api::client::sync::sync_events::v5 as http,
811 event_id,
812 events::{
813 room::{
814 member::RoomMemberEventContent,
815 message::{MessageFormat, MessageType},
816 },
817 AnySyncStateEvent, AnySyncTimelineEvent, BundledMessageLikeRelations,
818 },
819 room_id,
820 serde::Raw,
821 user_id, RoomId, UInt, UserId,
822 };
823
824 use super::{EventTimelineItem, Profile};
825 use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
826
827 #[async_test]
828 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
829 let room_id = room_id!("!q:x.uk");
832 let user_id = user_id!("@t:o.uk");
833 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
834 let client = logged_in_client(None).await;
835
836 let timeline_item =
838 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
839 .await
840 .unwrap();
841
842 assert_eq!(timeline_item.sender, user_id);
844 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
845 assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
846 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
847 assert_eq!(txt.body, "**My M**");
848 let formatted = txt.formatted.as_ref().unwrap();
849 assert_eq!(formatted.format, MessageFormat::Html);
850 assert_eq!(formatted.body, "<b>My M</b>");
851 } else {
852 panic!("Unexpected message type");
853 }
854 }
855
856 #[async_test]
857 async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
858 let room_id = room_id!("!q:x.uk");
862 let user_id = user_id!("@t:o.uk");
863 let raw_event = member_event_as_state_event(
864 room_id,
865 user_id,
866 "knock",
867 "Alice Margatroid",
868 "mxc://e.org/SEs",
869 );
870 let client = logged_in_client(None).await;
871
872 let power_level_event = sync_state_event!({
875 "type": "m.room.power_levels",
876 "content": {},
877 "event_id": "$143278582443PhrSn:example.org",
878 "origin_server_ts": 143273581,
879 "room_id": room_id,
880 "sender": user_id,
881 "state_key": "",
882 "unsigned": {
883 "age": 1234
884 }
885 });
886 let mut room = http::response::Room::new();
887 room.required_state.push(power_level_event);
888
889 let response = response_with_room(room_id, room);
891 client.process_sliding_sync_test_helper(&response).await.unwrap();
892
893 let event = TimelineEvent::new(raw_event.cast());
895 let timeline_item =
896 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
897 .await
898 .unwrap();
899
900 assert_eq!(timeline_item.sender, user_id);
902 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
903 assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
904 if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
905 assert_eq!(change.user_id, user_id);
906 assert_matches!(change.change, Some(MembershipChange::Knocked));
907 } else {
908 panic!("Unexpected state event type");
909 }
910 }
911
912 #[async_test]
913 async fn test_latest_message_includes_bundled_edit() {
914 let room_id = room_id!("!q:x.uk");
917 let user_id = user_id!("@t:o.uk");
918
919 let f = EventFactory::new();
920
921 let original_event_id = event_id!("$original");
922
923 let mut relations = BundledMessageLikeRelations::new();
924 relations.replace = Some(Box::new(
925 f.text_html(" * Updated!", " * <b>Updated!</b>")
926 .edit(
927 original_event_id,
928 MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
929 )
930 .event_id(event_id!("$edit"))
931 .sender(user_id)
932 .into_raw_sync(),
933 ));
934
935 let event = f
936 .text_html("**My M**", "<b>My M</b>")
937 .sender(user_id)
938 .event_id(original_event_id)
939 .bundled_relations(relations)
940 .server_ts(42)
941 .into_event();
942
943 let client = logged_in_client(None).await;
944
945 let timeline_item =
947 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
948 .await
949 .unwrap();
950
951 assert_eq!(timeline_item.sender, user_id);
953 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
954 assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
955 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
956 assert_eq!(txt.body, "Updated!");
957 let formatted = txt.formatted.as_ref().unwrap();
958 assert_eq!(formatted.format, MessageFormat::Html);
959 assert_eq!(formatted.body, "<b>Updated!</b>");
960 } else {
961 panic!("Unexpected message type");
962 }
963 }
964
965 #[async_test]
966 async fn test_latest_poll_includes_bundled_edit() {
967 let room_id = room_id!("!q:x.uk");
970 let user_id = user_id!("@t:o.uk");
971
972 let f = EventFactory::new();
973
974 let original_event_id = event_id!("$original");
975
976 let mut relations = BundledMessageLikeRelations::new();
977 relations.replace = Some(Box::new(
978 f.poll_edit(
979 original_event_id,
980 "It's one banana, Michael, how much could it cost?",
981 vec!["1 dollar", "10 dollars", "100 dollars"],
982 )
983 .event_id(event_id!("$edit"))
984 .sender(user_id)
985 .into_raw_sync(),
986 ));
987
988 let event = f
989 .poll_start(
990 "It's one avocado, Michael, how much could it cost? 10 dollars?",
991 "It's one avocado, Michael, how much could it cost?",
992 vec!["1 dollar", "10 dollars", "100 dollars"],
993 )
994 .event_id(original_event_id)
995 .bundled_relations(relations)
996 .sender(user_id)
997 .into_event();
998
999 let client = logged_in_client(None).await;
1000
1001 let timeline_item =
1003 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1004 .await
1005 .unwrap();
1006
1007 assert_eq!(timeline_item.sender, user_id);
1009
1010 let poll = timeline_item.content().as_poll().unwrap();
1011 assert!(poll.has_been_edited);
1012 assert_eq!(
1013 poll.start_event_content.poll_start.question.text,
1014 "It's one banana, Michael, how much could it cost?"
1015 );
1016 }
1017
1018 #[async_test]
1019 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
1020 ) {
1021 use ruma::owned_mxc_uri;
1025 let room_id = room_id!("!q:x.uk");
1026 let user_id = user_id!("@t:o.uk");
1027 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1028 let client = logged_in_client(None).await;
1029 let mut room = http::response::Room::new();
1030 room.required_state.push(member_event_as_state_event(
1031 room_id,
1032 user_id,
1033 "join",
1034 "Alice Margatroid",
1035 "mxc://e.org/SEs",
1036 ));
1037
1038 let response = response_with_room(room_id, room);
1040 client.process_sliding_sync_test_helper(&response).await.unwrap();
1041
1042 let timeline_item =
1044 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1045 .await
1046 .unwrap();
1047
1048 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1050 assert_eq!(
1051 profile,
1052 Profile {
1053 display_name: Some("Alice Margatroid".to_owned()),
1054 display_name_ambiguous: false,
1055 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1056 }
1057 );
1058 }
1059
1060 #[async_test]
1061 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
1062 ) {
1063 use ruma::owned_mxc_uri;
1067 let room_id = room_id!("!q:x.uk");
1068 let user_id = user_id!("@t:o.uk");
1069 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1070 let client = logged_in_client(None).await;
1071
1072 let member_event = MinimalStateEvent::Original(
1073 member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")
1074 .deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
1075 .unwrap(),
1076 );
1077
1078 let room = http::response::Room::new();
1079 let response = response_with_room(room_id, room);
1084 client.process_sliding_sync_test_helper(&response).await.unwrap();
1085
1086 let timeline_item = EventTimelineItem::from_latest_event(
1088 client,
1089 room_id,
1090 LatestEvent::new_with_sender_details(event, Some(member_event), None),
1091 )
1092 .await
1093 .unwrap();
1094
1095 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1097 assert_eq!(
1098 profile,
1099 Profile {
1100 display_name: Some("Alice Margatroid".to_owned()),
1101 display_name_ambiguous: false,
1102 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1103 }
1104 );
1105 }
1106
1107 #[async_test]
1108 async fn test_emoji_detection() {
1109 let room_id = room_id!("!q:x.uk");
1110 let user_id = user_id!("@t:o.uk");
1111 let client = logged_in_client(None).await;
1112
1113 let mut event = message_event(room_id, user_id, "π€·ββοΈ No boost π€·ββοΈ", "", 0);
1114 let mut timeline_item =
1115 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1116 .await
1117 .unwrap();
1118
1119 assert!(!timeline_item.contains_only_emojis());
1120
1121 event = message_event(room_id, user_id, " π ", "", 0);
1123 timeline_item =
1124 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1125 .await
1126 .unwrap();
1127
1128 assert!(timeline_item.contains_only_emojis());
1129
1130 event = message_event(room_id, user_id, "π¨βπ©βπ¦1οΈβ£ππ³πΎββοΈπͺ©πππ»π«±πΌβπ«²πΎππ", "", 0);
1132 timeline_item =
1133 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1134 .await
1135 .unwrap();
1136
1137 assert!(!timeline_item.contains_only_emojis());
1138
1139 event = message_event(room_id, user_id, "π¨βπ©βπ¦1οΈβ£π³πΎββοΈππ»π«±πΌβπ«²πΎ", "", 0);
1141 timeline_item =
1142 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1143 .await
1144 .unwrap();
1145
1146 assert!(timeline_item.contains_only_emojis());
1147 }
1148
1149 fn member_event(
1150 room_id: &RoomId,
1151 user_id: &UserId,
1152 display_name: &str,
1153 avatar_url: &str,
1154 ) -> Raw<AnySyncTimelineEvent> {
1155 sync_timeline_event!({
1156 "type": "m.room.member",
1157 "content": {
1158 "avatar_url": avatar_url,
1159 "displayname": display_name,
1160 "membership": "join",
1161 "reason": ""
1162 },
1163 "event_id": "$143273582443PhrSn:example.org",
1164 "origin_server_ts": 143273583,
1165 "room_id": room_id,
1166 "sender": "@example:example.org",
1167 "state_key": user_id,
1168 "type": "m.room.member",
1169 "unsigned": {
1170 "age": 1234
1171 }
1172 })
1173 }
1174
1175 fn member_event_as_state_event(
1176 room_id: &RoomId,
1177 user_id: &UserId,
1178 membership: &str,
1179 display_name: &str,
1180 avatar_url: &str,
1181 ) -> Raw<AnySyncStateEvent> {
1182 sync_state_event!({
1183 "type": "m.room.member",
1184 "content": {
1185 "avatar_url": avatar_url,
1186 "displayname": display_name,
1187 "membership": membership,
1188 "reason": ""
1189 },
1190 "event_id": "$143273582443PhrSn:example.org",
1191 "origin_server_ts": 143273583,
1192 "room_id": room_id,
1193 "sender": user_id,
1194 "state_key": user_id,
1195 "unsigned": {
1196 "age": 1234
1197 }
1198 })
1199 }
1200
1201 fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
1202 let mut response = http::Response::new("6".to_owned());
1203 response.rooms.insert(room_id.to_owned(), room);
1204 response
1205 }
1206
1207 fn message_event(
1208 room_id: &RoomId,
1209 user_id: &UserId,
1210 body: &str,
1211 formatted_body: &str,
1212 ts: u64,
1213 ) -> TimelineEvent {
1214 TimelineEvent::new(sync_timeline_event!({
1215 "event_id": "$eventid6",
1216 "sender": user_id,
1217 "origin_server_ts": ts,
1218 "type": "m.room.message",
1219 "room_id": room_id.to_string(),
1220 "content": {
1221 "body": body,
1222 "format": "org.matrix.custom.html",
1223 "formatted_body": formatted_body,
1224 "msgtype": "m.text"
1225 },
1226 }))
1227 }
1228}