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 ResponseData,
49 },
50 local::LocalEventTimelineItem,
51 remote::{RemoteEventOrigin, RemoteEventTimelineItem},
52};
53pub use self::{
54 content::{
55 AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange,
56 MembershipChange, Message, OtherState, PollResult, PollState, RepliedToEvent,
57 RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineItemContent,
58 },
59 local::EventSendState,
60};
61use super::{RepliedToInfo, ReplyContent, UnsupportedReplyItem};
62
63#[derive(Clone, Debug)]
69pub struct EventTimelineItem {
70 pub(super) sender: OwnedUserId,
72 pub(super) sender_profile: TimelineDetails<Profile>,
74 pub(super) reactions: ReactionsByKeyBySender,
76 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
78 pub(super) content: TimelineItemContent,
80 pub(super) kind: EventTimelineItemKind,
82 pub(super) is_room_encrypted: Option<bool>,
87}
88
89#[derive(Clone, Debug)]
90pub(super) enum EventTimelineItemKind {
91 Local(LocalEventTimelineItem),
93 Remote(RemoteEventTimelineItem),
95}
96
97#[derive(Clone, Debug, Eq, Hash, PartialEq)]
99pub enum TimelineEventItemId {
100 TransactionId(OwnedTransactionId),
103 EventId(OwnedEventId),
105}
106
107pub(crate) enum TimelineItemHandle<'a> {
113 Remote(&'a EventId),
114 Local(&'a SendHandle),
115}
116
117impl EventTimelineItem {
118 pub(super) fn new(
119 sender: OwnedUserId,
120 sender_profile: TimelineDetails<Profile>,
121 timestamp: MilliSecondsSinceUnixEpoch,
122 content: TimelineItemContent,
123 kind: EventTimelineItemKind,
124 reactions: ReactionsByKeyBySender,
125 is_room_encrypted: bool,
126 ) -> Self {
127 let is_room_encrypted = Some(is_room_encrypted);
128 Self { sender, sender_profile, timestamp, content, reactions, kind, is_room_encrypted }
129 }
130
131 pub async fn from_latest_event(
143 client: Client,
144 room_id: &RoomId,
145 latest_event: LatestEvent,
146 ) -> Option<EventTimelineItem> {
147 use super::traits::RoomDataProvider;
151
152 let raw_sync_event = latest_event.event().raw().clone();
153 let encryption_info = latest_event.event().encryption_info().cloned();
154
155 let Ok(event) = raw_sync_event.deserialize_as::<AnySyncTimelineEvent>() else {
156 warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!");
157 return None;
158 };
159
160 let timestamp = event.origin_server_ts();
161 let sender = event.sender().to_owned();
162 let event_id = event.event_id().to_owned();
163 let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false);
164
165 let power_levels = if let Some(room) = client.get_room(room_id) {
167 room.power_levels().await.ok()
168 } else {
169 None
170 };
171 let room_power_levels_info = client.user_id().zip(power_levels.as_ref());
172
173 let content =
176 TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?;
177
178 let reactions = ReactionsByKeyBySender::default();
181
182 let read_receipts = IndexMap::new();
184
185 let is_highlighted = false;
187
188 let latest_edit_json = None;
191
192 let origin = RemoteEventOrigin::Sync;
194
195 let kind = RemoteEventTimelineItem {
196 event_id,
197 transaction_id: None,
198 read_receipts,
199 is_own,
200 is_highlighted,
201 encryption_info,
202 original_json: Some(raw_sync_event),
203 latest_edit_json,
204 origin,
205 }
206 .into();
207
208 let room = client.get_room(room_id);
209 let sender_profile = if let Some(room) = room {
210 let mut profile = room.profile_from_latest_event(&latest_event);
211
212 if profile.is_none() {
214 profile = room.profile_from_user_id(&sender).await;
215 }
216
217 profile.map(TimelineDetails::Ready).unwrap_or(TimelineDetails::Unavailable)
218 } else {
219 TimelineDetails::Unavailable
220 };
221
222 Some(Self {
223 sender,
224 sender_profile,
225 timestamp,
226 content,
227 kind,
228 reactions,
229 is_room_encrypted: None,
230 })
231 }
232
233 pub fn is_local_echo(&self) -> bool {
240 matches!(self.kind, EventTimelineItemKind::Local(_))
241 }
242
243 pub fn is_remote_event(&self) -> bool {
251 matches!(self.kind, EventTimelineItemKind::Remote(_))
252 }
253
254 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
256 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
257 }
258
259 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
261 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
262 }
263
264 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
267 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
268 }
269
270 pub fn send_state(&self) -> Option<&EventSendState> {
272 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
273 }
274
275 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
277 match &self.kind {
278 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
279 EventTimelineItemKind::Remote(_) => None,
280 }
281 }
282
283 pub fn identifier(&self) -> TimelineEventItemId {
289 match &self.kind {
290 EventTimelineItemKind::Local(local) => local.identifier(),
291 EventTimelineItemKind::Remote(remote) => {
292 TimelineEventItemId::EventId(remote.event_id.clone())
293 }
294 }
295 }
296
297 pub fn transaction_id(&self) -> Option<&TransactionId> {
302 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
303 }
304
305 pub fn event_id(&self) -> Option<&EventId> {
314 match &self.kind {
315 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
316 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
317 }
318 }
319
320 pub fn sender(&self) -> &UserId {
322 &self.sender
323 }
324
325 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
327 &self.sender_profile
328 }
329
330 pub fn content(&self) -> &TimelineItemContent {
332 &self.content
333 }
334
335 pub fn reactions(&self) -> &ReactionsByKeyBySender {
337 &self.reactions
338 }
339
340 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
347 static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
348 match &self.kind {
349 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
350 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
351 }
352 }
353
354 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
360 self.timestamp
361 }
362
363 pub fn is_own(&self) -> bool {
365 match &self.kind {
366 EventTimelineItemKind::Local(_) => true,
367 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
368 }
369 }
370
371 pub fn is_editable(&self) -> bool {
373 if !self.is_own() {
377 return false;
379 }
380
381 match self.content() {
382 TimelineItemContent::Message(message) => {
383 matches!(
384 message.msgtype(),
385 MessageType::Text(_)
386 | MessageType::Emote(_)
387 | MessageType::Audio(_)
388 | MessageType::File(_)
389 | MessageType::Image(_)
390 | MessageType::Video(_)
391 )
392 }
393 TimelineItemContent::Poll(poll) => {
394 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
395 }
396 _ => {
397 false
399 }
400 }
401 }
402
403 pub fn is_highlighted(&self) -> bool {
405 match &self.kind {
406 EventTimelineItemKind::Local(_) => false,
407 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
408 }
409 }
410
411 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
413 match &self.kind {
414 EventTimelineItemKind::Local(_) => None,
415 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_ref(),
416 }
417 }
418
419 pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
422 if self.is_room_encrypted != Some(true) || self.is_local_echo() {
423 return None;
424 }
425
426 if let TimelineItemContent::UnableToDecrypt(_) = self.content() {
428 return None;
429 }
430
431 match self.encryption_info() {
432 Some(info) => {
433 if strict {
434 Some(info.verification_state.to_shield_state_strict())
435 } else {
436 Some(info.verification_state.to_shield_state_lax())
437 }
438 }
439 None => Some(ShieldState::Red {
440 code: ShieldStateCode::SentInClear,
441 message: SENT_IN_CLEAR,
442 }),
443 }
444 }
445
446 pub fn can_be_replied_to(&self) -> bool {
448 if self.event_id().is_none() {
450 false
451 } else if let TimelineItemContent::Message(_) = self.content() {
452 true
453 } else {
454 self.latest_json().is_some()
455 }
456 }
457
458 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
464 match &self.kind {
465 EventTimelineItemKind::Local(_) => None,
466 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
467 }
468 }
469
470 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
472 match &self.kind {
473 EventTimelineItemKind::Local(_) => None,
474 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
475 }
476 }
477
478 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
481 self.latest_edit_json().or_else(|| self.original_json())
482 }
483
484 pub fn origin(&self) -> Option<EventItemOrigin> {
488 match &self.kind {
489 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
490 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
491 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
492 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
493 _ => None,
494 },
495 }
496 }
497
498 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
499 self.content = content;
500 }
501
502 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
504 Self { kind: kind.into(), ..self.clone() }
505 }
506
507 pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
509 Self { reactions, ..self.clone() }
510 }
511
512 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
514 let mut new = self.clone();
515 new.content = new_content;
516 new
517 }
518
519 pub(super) fn with_content_and_latest_edit(
524 &self,
525 new_content: TimelineItemContent,
526 edit_json: Option<Raw<AnySyncTimelineEvent>>,
527 ) -> Self {
528 let mut new = self.clone();
529 new.content = new_content;
530 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
531 r.latest_edit_json = edit_json;
532 }
533 new
534 }
535
536 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
538 Self { sender_profile, ..self.clone() }
539 }
540
541 pub(super) fn with_encryption_info(&self, encryption_info: Option<EncryptionInfo>) -> Self {
543 let mut new = self.clone();
544 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
545 r.encryption_info = encryption_info;
546 }
547
548 new
549 }
550
551 pub(super) fn redact(&self, room_version: &RoomVersionId) -> Self {
553 let content = self.content.redact(room_version);
554 let kind = match &self.kind {
555 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
556 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
557 };
558 Self {
559 sender: self.sender.clone(),
560 sender_profile: self.sender_profile.clone(),
561 timestamp: self.timestamp,
562 content,
563 kind,
564 is_room_encrypted: self.is_room_encrypted,
565 reactions: ReactionsByKeyBySender::default(),
566 }
567 }
568
569 pub fn replied_to_info(&self) -> Result<RepliedToInfo, UnsupportedReplyItem> {
571 let reply_content = match self.content() {
572 TimelineItemContent::Message(msg) => ReplyContent::Message(msg.to_owned()),
573 _ => {
574 let Some(raw_event) = self.latest_json() else {
575 return Err(UnsupportedReplyItem::MissingJson);
576 };
577
578 ReplyContent::Raw(raw_event.clone())
579 }
580 };
581
582 let Some(event_id) = self.event_id() else {
583 return Err(UnsupportedReplyItem::MissingEventId);
584 };
585
586 Ok(RepliedToInfo {
587 event_id: event_id.to_owned(),
588 sender: self.sender().to_owned(),
589 timestamp: self.timestamp(),
590 content: reply_content,
591 })
592 }
593
594 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
595 match &self.kind {
596 EventTimelineItemKind::Local(local) => {
597 if let Some(event_id) = local.event_id() {
598 TimelineItemHandle::Remote(event_id)
599 } else {
600 TimelineItemHandle::Local(
601 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
603 )
604 }
605 }
606 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
607 }
608 }
609
610 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
612 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
613 }
614
615 pub fn contains_only_emojis(&self) -> bool {
638 let body = match self.content() {
639 TimelineItemContent::Message(msg) => match msg.msgtype() {
640 MessageType::Text(text) => Some(text.body.as_str()),
641 MessageType::Audio(audio) => audio.caption(),
642 MessageType::File(file) => file.caption(),
643 MessageType::Image(image) => image.caption(),
644 MessageType::Video(video) => video.caption(),
645 _ => None,
646 },
647 TimelineItemContent::RedactedMessage
648 | TimelineItemContent::Sticker(_)
649 | TimelineItemContent::UnableToDecrypt(_)
650 | TimelineItemContent::MembershipChange(_)
651 | TimelineItemContent::ProfileChange(_)
652 | TimelineItemContent::OtherState(_)
653 | TimelineItemContent::FailedToParseMessageLike { .. }
654 | TimelineItemContent::FailedToParseState { .. }
655 | TimelineItemContent::Poll(_)
656 | TimelineItemContent::CallInvite
657 | TimelineItemContent::CallNotify => None,
658 };
659
660 if let Some(body) = body {
661 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
663
664 if graphemes.len() > 5 {
669 return false;
670 }
671
672 graphemes.iter().all(|g| emojis::get(g).is_some())
673 } else {
674 false
675 }
676 }
677}
678
679impl From<LocalEventTimelineItem> for EventTimelineItemKind {
680 fn from(value: LocalEventTimelineItem) -> Self {
681 EventTimelineItemKind::Local(value)
682 }
683}
684
685impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
686 fn from(value: RemoteEventTimelineItem) -> Self {
687 EventTimelineItemKind::Remote(value)
688 }
689}
690
691#[derive(Clone, Debug, Default, PartialEq, Eq)]
693pub struct Profile {
694 pub display_name: Option<String>,
696
697 pub display_name_ambiguous: bool,
703
704 pub avatar_url: Option<OwnedMxcUri>,
706}
707
708#[derive(Clone, Debug)]
712pub enum TimelineDetails<T> {
713 Unavailable,
716
717 Pending,
719
720 Ready(T),
722
723 Error(Arc<Error>),
725}
726
727impl<T> TimelineDetails<T> {
728 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
729 match value {
730 Some(v) => Self::Ready(v),
731 None => Self::Unavailable,
732 }
733 }
734
735 pub(crate) fn is_unavailable(&self) -> bool {
736 matches!(self, Self::Unavailable)
737 }
738
739 pub fn is_ready(&self) -> bool {
740 matches!(self, Self::Ready(_))
741 }
742}
743
744#[derive(Clone, Copy, Debug)]
746#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
747pub enum EventItemOrigin {
748 Local,
750 Sync,
752 Pagination,
754}
755
756#[derive(Clone, Debug)]
758pub enum ReactionStatus {
759 LocalToLocal(Option<SendReactionHandle>),
763 LocalToRemote(Option<SendHandle>),
767 RemoteToRemote(OwnedEventId),
769}
770
771#[derive(Clone, Debug)]
773pub struct ReactionInfo {
774 pub timestamp: MilliSecondsSinceUnixEpoch,
775 pub status: ReactionStatus,
777}
778
779#[derive(Debug, Clone, Default)]
784pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
785
786impl Deref for ReactionsByKeyBySender {
787 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
788
789 fn deref(&self) -> &Self::Target {
790 &self.0
791 }
792}
793
794impl DerefMut for ReactionsByKeyBySender {
795 fn deref_mut(&mut self) -> &mut Self::Target {
796 &mut self.0
797 }
798}
799
800impl ReactionsByKeyBySender {
801 pub(crate) fn remove_reaction(
807 &mut self,
808 sender: &UserId,
809 annotation: &str,
810 ) -> Option<ReactionInfo> {
811 if let Some(by_user) = self.0.get_mut(annotation) {
812 if let Some(info) = by_user.swap_remove(sender) {
813 if by_user.is_empty() {
815 self.0.swap_remove(annotation);
816 }
817 return Some(info);
818 }
819 }
820 None
821 }
822}
823
824#[cfg(test)]
825mod tests {
826 use assert_matches::assert_matches;
827 use assert_matches2::assert_let;
828 use matrix_sdk::test_utils::logged_in_client;
829 use matrix_sdk_base::{
830 deserialized_responses::TimelineEvent, latest_event::LatestEvent, sliding_sync::http,
831 MinimalStateEvent, OriginalMinimalStateEvent,
832 };
833 use matrix_sdk_test::{
834 async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event,
835 };
836 use ruma::{
837 event_id,
838 events::{
839 room::{
840 member::RoomMemberEventContent,
841 message::{MessageFormat, MessageType},
842 },
843 AnySyncStateEvent, AnySyncTimelineEvent, BundledMessageLikeRelations,
844 },
845 room_id,
846 serde::Raw,
847 user_id, RoomId, UInt, UserId,
848 };
849
850 use super::{EventTimelineItem, Profile};
851 use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
852
853 #[async_test]
854 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
855 let room_id = room_id!("!q:x.uk");
858 let user_id = user_id!("@t:o.uk");
859 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
860 let client = logged_in_client(None).await;
861
862 let timeline_item =
864 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
865 .await
866 .unwrap();
867
868 assert_eq!(timeline_item.sender, user_id);
870 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
871 assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
872 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
873 assert_eq!(txt.body, "**My M**");
874 let formatted = txt.formatted.as_ref().unwrap();
875 assert_eq!(formatted.format, MessageFormat::Html);
876 assert_eq!(formatted.body, "<b>My M</b>");
877 } else {
878 panic!("Unexpected message type");
879 }
880 }
881
882 #[async_test]
883 async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
884 let room_id = room_id!("!q:x.uk");
888 let user_id = user_id!("@t:o.uk");
889 let raw_event = member_event_as_state_event(
890 room_id,
891 user_id,
892 "knock",
893 "Alice Margatroid",
894 "mxc://e.org/SEs",
895 );
896 let client = logged_in_client(None).await;
897
898 let power_level_event = sync_state_event!({
901 "type": "m.room.power_levels",
902 "content": {},
903 "event_id": "$143278582443PhrSn:example.org",
904 "origin_server_ts": 143273581,
905 "room_id": room_id,
906 "sender": user_id,
907 "state_key": "",
908 "unsigned": {
909 "age": 1234
910 }
911 });
912 let mut room = http::response::Room::new();
913 room.required_state.push(power_level_event);
914
915 let response = response_with_room(room_id, room);
917 client.process_sliding_sync_test_helper(&response).await.unwrap();
918
919 let event = TimelineEvent::new(raw_event.cast());
921 let timeline_item =
922 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
923 .await
924 .unwrap();
925
926 assert_eq!(timeline_item.sender, user_id);
928 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
929 assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
930 if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
931 assert_eq!(change.user_id, user_id);
932 assert_matches!(change.change, Some(MembershipChange::Knocked));
933 } else {
934 panic!("Unexpected state event type");
935 }
936 }
937
938 #[async_test]
939 async fn test_latest_message_includes_bundled_edit() {
940 let room_id = room_id!("!q:x.uk");
943 let user_id = user_id!("@t:o.uk");
944
945 let f = EventFactory::new();
946
947 let original_event_id = event_id!("$original");
948
949 let mut relations = BundledMessageLikeRelations::new();
950 relations.replace = Some(Box::new(
951 f.text_html(" * Updated!", " * <b>Updated!</b>")
952 .edit(
953 original_event_id,
954 MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
955 )
956 .event_id(event_id!("$edit"))
957 .sender(user_id)
958 .into_raw_sync(),
959 ));
960
961 let event = f
962 .text_html("**My M**", "<b>My M</b>")
963 .sender(user_id)
964 .event_id(original_event_id)
965 .bundled_relations(relations)
966 .server_ts(42)
967 .into_event();
968
969 let client = logged_in_client(None).await;
970
971 let timeline_item =
973 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
974 .await
975 .unwrap();
976
977 assert_eq!(timeline_item.sender, user_id);
979 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
980 assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
981 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
982 assert_eq!(txt.body, "Updated!");
983 let formatted = txt.formatted.as_ref().unwrap();
984 assert_eq!(formatted.format, MessageFormat::Html);
985 assert_eq!(formatted.body, "<b>Updated!</b>");
986 } else {
987 panic!("Unexpected message type");
988 }
989 }
990
991 #[async_test]
992 async fn test_latest_poll_includes_bundled_edit() {
993 let room_id = room_id!("!q:x.uk");
996 let user_id = user_id!("@t:o.uk");
997
998 let f = EventFactory::new();
999
1000 let original_event_id = event_id!("$original");
1001
1002 let mut relations = BundledMessageLikeRelations::new();
1003 relations.replace = Some(Box::new(
1004 f.poll_edit(
1005 original_event_id,
1006 "It's one banana, Michael, how much could it cost?",
1007 vec!["1 dollar", "10 dollars", "100 dollars"],
1008 )
1009 .event_id(event_id!("$edit"))
1010 .sender(user_id)
1011 .into_raw_sync(),
1012 ));
1013
1014 let event = f
1015 .poll_start(
1016 "It's one avocado, Michael, how much could it cost? 10 dollars?",
1017 "It's one avocado, Michael, how much could it cost?",
1018 vec!["1 dollar", "10 dollars", "100 dollars"],
1019 )
1020 .event_id(original_event_id)
1021 .bundled_relations(relations)
1022 .sender(user_id)
1023 .into_event();
1024
1025 let client = logged_in_client(None).await;
1026
1027 let timeline_item =
1029 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1030 .await
1031 .unwrap();
1032
1033 assert_eq!(timeline_item.sender, user_id);
1035
1036 let poll = timeline_item.content().as_poll().unwrap();
1037 assert!(poll.has_been_edited);
1038 assert_eq!(
1039 poll.start_event_content.poll_start.question.text,
1040 "It's one banana, Michael, how much could it cost?"
1041 );
1042 }
1043
1044 #[async_test]
1045 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
1046 ) {
1047 use ruma::owned_mxc_uri;
1051 let room_id = room_id!("!q:x.uk");
1052 let user_id = user_id!("@t:o.uk");
1053 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1054 let client = logged_in_client(None).await;
1055 let mut room = http::response::Room::new();
1056 room.required_state.push(member_event_as_state_event(
1057 room_id,
1058 user_id,
1059 "join",
1060 "Alice Margatroid",
1061 "mxc://e.org/SEs",
1062 ));
1063
1064 let response = response_with_room(room_id, room);
1066 client.process_sliding_sync_test_helper(&response).await.unwrap();
1067
1068 let timeline_item =
1070 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1071 .await
1072 .unwrap();
1073
1074 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1076 assert_eq!(
1077 profile,
1078 Profile {
1079 display_name: Some("Alice Margatroid".to_owned()),
1080 display_name_ambiguous: false,
1081 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1082 }
1083 );
1084 }
1085
1086 #[async_test]
1087 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
1088 ) {
1089 use ruma::owned_mxc_uri;
1093 let room_id = room_id!("!q:x.uk");
1094 let user_id = user_id!("@t:o.uk");
1095 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1096 let client = logged_in_client(None).await;
1097
1098 let member_event = MinimalStateEvent::Original(
1099 member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")
1100 .deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
1101 .unwrap(),
1102 );
1103
1104 let room = http::response::Room::new();
1105 let response = response_with_room(room_id, room);
1110 client.process_sliding_sync_test_helper(&response).await.unwrap();
1111
1112 let timeline_item = EventTimelineItem::from_latest_event(
1114 client,
1115 room_id,
1116 LatestEvent::new_with_sender_details(event, Some(member_event), None),
1117 )
1118 .await
1119 .unwrap();
1120
1121 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1123 assert_eq!(
1124 profile,
1125 Profile {
1126 display_name: Some("Alice Margatroid".to_owned()),
1127 display_name_ambiguous: false,
1128 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1129 }
1130 );
1131 }
1132
1133 #[async_test]
1134 async fn test_emoji_detection() {
1135 let room_id = room_id!("!q:x.uk");
1136 let user_id = user_id!("@t:o.uk");
1137 let client = logged_in_client(None).await;
1138
1139 let mut event = message_event(room_id, user_id, "π€·ββοΈ No boost π€·ββοΈ", "", 0);
1140 let mut timeline_item =
1141 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1142 .await
1143 .unwrap();
1144
1145 assert!(!timeline_item.contains_only_emojis());
1146
1147 event = message_event(room_id, user_id, " π ", "", 0);
1149 timeline_item =
1150 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1151 .await
1152 .unwrap();
1153
1154 assert!(timeline_item.contains_only_emojis());
1155
1156 event = message_event(room_id, user_id, "π¨βπ©βπ¦1οΈβ£ππ³πΎββοΈπͺ©πππ»π«±πΌβπ«²πΎππ", "", 0);
1158 timeline_item =
1159 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1160 .await
1161 .unwrap();
1162
1163 assert!(!timeline_item.contains_only_emojis());
1164
1165 event = message_event(room_id, user_id, "π¨βπ©βπ¦1οΈβ£π³πΎββοΈππ»π«±πΌβπ«²πΎ", "", 0);
1167 timeline_item =
1168 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1169 .await
1170 .unwrap();
1171
1172 assert!(timeline_item.contains_only_emojis());
1173 }
1174
1175 fn member_event(
1176 room_id: &RoomId,
1177 user_id: &UserId,
1178 display_name: &str,
1179 avatar_url: &str,
1180 ) -> Raw<AnySyncTimelineEvent> {
1181 sync_timeline_event!({
1182 "type": "m.room.member",
1183 "content": {
1184 "avatar_url": avatar_url,
1185 "displayname": display_name,
1186 "membership": "join",
1187 "reason": ""
1188 },
1189 "event_id": "$143273582443PhrSn:example.org",
1190 "origin_server_ts": 143273583,
1191 "room_id": room_id,
1192 "sender": "@example:example.org",
1193 "state_key": user_id,
1194 "type": "m.room.member",
1195 "unsigned": {
1196 "age": 1234
1197 }
1198 })
1199 }
1200
1201 fn member_event_as_state_event(
1202 room_id: &RoomId,
1203 user_id: &UserId,
1204 membership: &str,
1205 display_name: &str,
1206 avatar_url: &str,
1207 ) -> Raw<AnySyncStateEvent> {
1208 sync_state_event!({
1209 "type": "m.room.member",
1210 "content": {
1211 "avatar_url": avatar_url,
1212 "displayname": display_name,
1213 "membership": membership,
1214 "reason": ""
1215 },
1216 "event_id": "$143273582443PhrSn:example.org",
1217 "origin_server_ts": 143273583,
1218 "room_id": room_id,
1219 "sender": user_id,
1220 "state_key": user_id,
1221 "unsigned": {
1222 "age": 1234
1223 }
1224 })
1225 }
1226
1227 fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
1228 let mut response = http::Response::new("6".to_owned());
1229 response.rooms.insert(room_id.to_owned(), room);
1230 response
1231 }
1232
1233 fn message_event(
1234 room_id: &RoomId,
1235 user_id: &UserId,
1236 body: &str,
1237 formatted_body: &str,
1238 ts: u64,
1239 ) -> TimelineEvent {
1240 TimelineEvent::new(sync_timeline_event!({
1241 "event_id": "$eventid6",
1242 "sender": user_id,
1243 "origin_server_ts": ts,
1244 "type": "m.room.message",
1245 "room_id": room_id.to_string(),
1246 "content": {
1247 "body": body,
1248 "format": "org.matrix.custom.html",
1249 "formatted_body": formatted_body,
1250 "msgtype": "m.text"
1251 },
1252 }))
1253 }
1254}