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, MsgLikeContent, MsgLikeKind, OtherState, PollResult, PollState,
56 RepliedToEvent, RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineItemContent,
57 },
58 local::EventSendState,
59};
60
61#[derive(Clone, Debug)]
67pub struct EventTimelineItem {
68 pub(super) sender: OwnedUserId,
70 pub(super) sender_profile: TimelineDetails<Profile>,
72 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
74 pub(super) content: TimelineItemContent,
76 pub(super) kind: EventTimelineItemKind,
78 pub(super) is_room_encrypted: bool,
82}
83
84#[derive(Clone, Debug)]
85pub(super) enum EventTimelineItemKind {
86 Local(LocalEventTimelineItem),
88 Remote(RemoteEventTimelineItem),
90}
91
92#[derive(Clone, Debug, Eq, Hash, PartialEq)]
94pub enum TimelineEventItemId {
95 TransactionId(OwnedTransactionId),
98 EventId(OwnedEventId),
100}
101
102pub(crate) enum TimelineItemHandle<'a> {
108 Remote(&'a EventId),
109 Local(&'a SendHandle),
110}
111
112impl EventTimelineItem {
113 pub(super) fn new(
114 sender: OwnedUserId,
115 sender_profile: TimelineDetails<Profile>,
116 timestamp: MilliSecondsSinceUnixEpoch,
117 content: TimelineItemContent,
118 kind: EventTimelineItemKind,
119 is_room_encrypted: bool,
120 ) -> Self {
121 Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted }
122 }
123
124 pub async fn from_latest_event(
136 client: Client,
137 room_id: &RoomId,
138 latest_event: LatestEvent,
139 ) -> Option<EventTimelineItem> {
140 use super::traits::RoomDataProvider;
144
145 let raw_sync_event = latest_event.event().raw().clone();
146 let encryption_info = latest_event.event().encryption_info().cloned();
147
148 let Ok(event) = raw_sync_event.deserialize_as::<AnySyncTimelineEvent>() else {
149 warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!");
150 return None;
151 };
152
153 let timestamp = event.origin_server_ts();
154 let sender = event.sender().to_owned();
155 let event_id = event.event_id().to_owned();
156 let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false);
157
158 let power_levels = if let Some(room) = client.get_room(room_id) {
160 room.power_levels().await.ok()
161 } else {
162 None
163 };
164 let room_power_levels_info = client.user_id().zip(power_levels.as_ref());
165
166 let content =
169 TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?;
170
171 let read_receipts = IndexMap::new();
173
174 let is_highlighted = false;
176
177 let latest_edit_json = None;
180
181 let origin = RemoteEventOrigin::Sync;
183
184 let kind = RemoteEventTimelineItem {
185 event_id,
186 transaction_id: None,
187 read_receipts,
188 is_own,
189 is_highlighted,
190 encryption_info,
191 original_json: Some(raw_sync_event),
192 latest_edit_json,
193 origin,
194 }
195 .into();
196
197 let room = client.get_room(room_id);
198 let sender_profile = if let Some(room) = room {
199 let mut profile = room.profile_from_latest_event(&latest_event);
200
201 if profile.is_none() {
203 profile = room.profile_from_user_id(&sender).await;
204 }
205
206 profile.map(TimelineDetails::Ready).unwrap_or(TimelineDetails::Unavailable)
207 } else {
208 TimelineDetails::Unavailable
209 };
210
211 Some(Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted: false })
212 }
213
214 pub fn is_local_echo(&self) -> bool {
221 matches!(self.kind, EventTimelineItemKind::Local(_))
222 }
223
224 pub fn is_remote_event(&self) -> bool {
232 matches!(self.kind, EventTimelineItemKind::Remote(_))
233 }
234
235 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
237 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
238 }
239
240 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
242 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
243 }
244
245 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
248 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
249 }
250
251 pub fn send_state(&self) -> Option<&EventSendState> {
253 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
254 }
255
256 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
258 match &self.kind {
259 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
260 EventTimelineItemKind::Remote(_) => None,
261 }
262 }
263
264 pub fn identifier(&self) -> TimelineEventItemId {
270 match &self.kind {
271 EventTimelineItemKind::Local(local) => local.identifier(),
272 EventTimelineItemKind::Remote(remote) => {
273 TimelineEventItemId::EventId(remote.event_id.clone())
274 }
275 }
276 }
277
278 pub fn transaction_id(&self) -> Option<&TransactionId> {
283 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
284 }
285
286 pub fn event_id(&self) -> Option<&EventId> {
295 match &self.kind {
296 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
297 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
298 }
299 }
300
301 pub fn sender(&self) -> &UserId {
303 &self.sender
304 }
305
306 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
308 &self.sender_profile
309 }
310
311 pub fn content(&self) -> &TimelineItemContent {
313 &self.content
314 }
315
316 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
323 static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
324 match &self.kind {
325 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
326 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
327 }
328 }
329
330 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
336 self.timestamp
337 }
338
339 pub fn is_own(&self) -> bool {
341 match &self.kind {
342 EventTimelineItemKind::Local(_) => true,
343 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
344 }
345 }
346
347 pub fn is_editable(&self) -> bool {
349 if !self.is_own() {
353 return false;
355 }
356
357 match self.content() {
358 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
359 MsgLikeKind::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 MsgLikeKind::Poll(poll) => {
371 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
372 }
373 _ => false,
375 },
376 _ => {
377 false
379 }
380 }
381 }
382
383 pub fn is_highlighted(&self) -> bool {
385 match &self.kind {
386 EventTimelineItemKind::Local(_) => false,
387 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
388 }
389 }
390
391 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
393 match &self.kind {
394 EventTimelineItemKind::Local(_) => None,
395 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_ref(),
396 }
397 }
398
399 pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
402 if !self.is_room_encrypted || self.is_local_echo() {
403 return None;
404 }
405
406 if let TimelineItemContent::UnableToDecrypt(_) = self.content() {
408 return None;
409 }
410
411 match self.encryption_info() {
412 Some(info) => {
413 if strict {
414 Some(info.verification_state.to_shield_state_strict())
415 } else {
416 Some(info.verification_state.to_shield_state_lax())
417 }
418 }
419 None => Some(ShieldState::Red {
420 code: ShieldStateCode::SentInClear,
421 message: SENT_IN_CLEAR,
422 }),
423 }
424 }
425
426 pub fn can_be_replied_to(&self) -> bool {
428 if self.event_id().is_none() {
430 false
431 } else if self.content.is_message() {
432 true
433 } else {
434 self.latest_json().is_some()
435 }
436 }
437
438 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
444 match &self.kind {
445 EventTimelineItemKind::Local(_) => None,
446 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
447 }
448 }
449
450 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
452 match &self.kind {
453 EventTimelineItemKind::Local(_) => None,
454 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
455 }
456 }
457
458 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
461 self.latest_edit_json().or_else(|| self.original_json())
462 }
463
464 pub fn origin(&self) -> Option<EventItemOrigin> {
468 match &self.kind {
469 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
470 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
471 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
472 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
473 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
474 RemoteEventOrigin::Unknown => None,
475 },
476 }
477 }
478
479 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
480 self.content = content;
481 }
482
483 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
485 Self { kind: kind.into(), ..self.clone() }
486 }
487
488 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
490 let mut new = self.clone();
491 new.content = new_content;
492 new
493 }
494
495 pub(super) fn with_content_and_latest_edit(
500 &self,
501 new_content: TimelineItemContent,
502 edit_json: Option<Raw<AnySyncTimelineEvent>>,
503 ) -> Self {
504 let mut new = self.clone();
505 new.content = new_content;
506 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
507 r.latest_edit_json = edit_json;
508 }
509 new
510 }
511
512 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
514 Self { sender_profile, ..self.clone() }
515 }
516
517 pub(super) fn with_encryption_info(&self, encryption_info: Option<EncryptionInfo>) -> Self {
519 let mut new = self.clone();
520 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
521 r.encryption_info = encryption_info;
522 }
523
524 new
525 }
526
527 pub(super) fn redact(&self, room_version: &RoomVersionId) -> Self {
529 let content = self.content.redact(room_version);
530 let kind = match &self.kind {
531 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
532 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
533 };
534 Self {
535 sender: self.sender.clone(),
536 sender_profile: self.sender_profile.clone(),
537 timestamp: self.timestamp,
538 content,
539 kind,
540 is_room_encrypted: self.is_room_encrypted,
541 }
542 }
543
544 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
545 match &self.kind {
546 EventTimelineItemKind::Local(local) => {
547 if let Some(event_id) = local.event_id() {
548 TimelineItemHandle::Remote(event_id)
549 } else {
550 TimelineItemHandle::Local(
551 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
553 )
554 }
555 }
556 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
557 }
558 }
559
560 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
562 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
563 }
564
565 pub fn contains_only_emojis(&self) -> bool {
588 let body = match self.content() {
589 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
590 MsgLikeKind::Message(message) => match &message.msgtype {
591 MessageType::Text(text) => Some(text.body.as_str()),
592 MessageType::Audio(audio) => audio.caption(),
593 MessageType::File(file) => file.caption(),
594 MessageType::Image(image) => image.caption(),
595 MessageType::Video(video) => video.caption(),
596 _ => None,
597 },
598 MsgLikeKind::Sticker(_) | MsgLikeKind::Poll(_) => None,
599 },
600 TimelineItemContent::RedactedMessage
601 | TimelineItemContent::UnableToDecrypt(_)
602 | TimelineItemContent::MembershipChange(_)
603 | TimelineItemContent::ProfileChange(_)
604 | TimelineItemContent::OtherState(_)
605 | TimelineItemContent::FailedToParseMessageLike { .. }
606 | TimelineItemContent::FailedToParseState { .. }
607 | TimelineItemContent::CallInvite
608 | TimelineItemContent::CallNotify => None,
609 };
610
611 if let Some(body) = body {
612 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
614
615 if graphemes.len() > 5 {
620 return false;
621 }
622
623 graphemes.iter().all(|g| emojis::get(g).is_some())
624 } else {
625 false
626 }
627 }
628}
629
630impl From<LocalEventTimelineItem> for EventTimelineItemKind {
631 fn from(value: LocalEventTimelineItem) -> Self {
632 EventTimelineItemKind::Local(value)
633 }
634}
635
636impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
637 fn from(value: RemoteEventTimelineItem) -> Self {
638 EventTimelineItemKind::Remote(value)
639 }
640}
641
642#[derive(Clone, Debug, Default, PartialEq, Eq)]
644pub struct Profile {
645 pub display_name: Option<String>,
647
648 pub display_name_ambiguous: bool,
654
655 pub avatar_url: Option<OwnedMxcUri>,
657}
658
659#[derive(Clone, Debug)]
663pub enum TimelineDetails<T> {
664 Unavailable,
667
668 Pending,
670
671 Ready(T),
673
674 Error(Arc<Error>),
676}
677
678impl<T> TimelineDetails<T> {
679 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
680 match value {
681 Some(v) => Self::Ready(v),
682 None => Self::Unavailable,
683 }
684 }
685
686 pub(crate) fn is_unavailable(&self) -> bool {
687 matches!(self, Self::Unavailable)
688 }
689
690 pub fn is_ready(&self) -> bool {
691 matches!(self, Self::Ready(_))
692 }
693}
694
695#[derive(Clone, Copy, Debug)]
697#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
698pub enum EventItemOrigin {
699 Local,
701 Sync,
703 Pagination,
705 Cache,
707}
708
709#[derive(Clone, Debug)]
711pub enum ReactionStatus {
712 LocalToLocal(Option<SendReactionHandle>),
716 LocalToRemote(Option<SendHandle>),
720 RemoteToRemote(OwnedEventId),
724}
725
726#[derive(Clone, Debug)]
728pub struct ReactionInfo {
729 pub timestamp: MilliSecondsSinceUnixEpoch,
730 pub status: ReactionStatus,
732}
733
734#[derive(Debug, Clone, Default)]
739pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
740
741impl Deref for ReactionsByKeyBySender {
742 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
743
744 fn deref(&self) -> &Self::Target {
745 &self.0
746 }
747}
748
749impl DerefMut for ReactionsByKeyBySender {
750 fn deref_mut(&mut self) -> &mut Self::Target {
751 &mut self.0
752 }
753}
754
755impl ReactionsByKeyBySender {
756 pub(crate) fn remove_reaction(
762 &mut self,
763 sender: &UserId,
764 annotation: &str,
765 ) -> Option<ReactionInfo> {
766 if let Some(by_user) = self.0.get_mut(annotation) {
767 if let Some(info) = by_user.swap_remove(sender) {
768 if by_user.is_empty() {
770 self.0.swap_remove(annotation);
771 }
772 return Some(info);
773 }
774 }
775 None
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use assert_matches::assert_matches;
782 use assert_matches2::assert_let;
783 use matrix_sdk::test_utils::logged_in_client;
784 use matrix_sdk_base::{
785 deserialized_responses::TimelineEvent, latest_event::LatestEvent, MinimalStateEvent,
786 OriginalMinimalStateEvent, RequestedRequiredStates,
787 };
788 use matrix_sdk_test::{
789 async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event,
790 };
791 use ruma::{
792 api::client::sync::sync_events::v5 as http,
793 event_id,
794 events::{
795 room::{
796 member::RoomMemberEventContent,
797 message::{MessageFormat, MessageType},
798 },
799 AnySyncStateEvent, AnySyncTimelineEvent, BundledMessageLikeRelations,
800 },
801 room_id,
802 serde::Raw,
803 user_id, RoomId, UInt, UserId,
804 };
805
806 use super::{EventTimelineItem, Profile};
807 use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
808
809 #[async_test]
810 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
811 let room_id = room_id!("!q:x.uk");
814 let user_id = user_id!("@t:o.uk");
815 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
816 let client = logged_in_client(None).await;
817
818 let timeline_item =
820 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
821 .await
822 .unwrap();
823
824 assert_eq!(timeline_item.sender, user_id);
826 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
827 assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
828 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
829 assert_eq!(txt.body, "**My M**");
830 let formatted = txt.formatted.as_ref().unwrap();
831 assert_eq!(formatted.format, MessageFormat::Html);
832 assert_eq!(formatted.body, "<b>My M</b>");
833 } else {
834 panic!("Unexpected message type");
835 }
836 }
837
838 #[async_test]
839 async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
840 let room_id = room_id!("!q:x.uk");
844 let user_id = user_id!("@t:o.uk");
845 let raw_event = member_event_as_state_event(
846 room_id,
847 user_id,
848 "knock",
849 "Alice Margatroid",
850 "mxc://e.org/SEs",
851 );
852 let client = logged_in_client(None).await;
853
854 let power_level_event = sync_state_event!({
857 "type": "m.room.power_levels",
858 "content": {},
859 "event_id": "$143278582443PhrSn:example.org",
860 "origin_server_ts": 143273581,
861 "room_id": room_id,
862 "sender": user_id,
863 "state_key": "",
864 "unsigned": {
865 "age": 1234
866 }
867 });
868 let mut room = http::response::Room::new();
869 room.required_state.push(power_level_event);
870
871 let response = response_with_room(room_id, room);
873 client
874 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
875 .await
876 .unwrap();
877
878 let event = TimelineEvent::new(raw_event.cast());
880 let timeline_item =
881 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
882 .await
883 .unwrap();
884
885 assert_eq!(timeline_item.sender, user_id);
887 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
888 assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
889 if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
890 assert_eq!(change.user_id, user_id);
891 assert_matches!(change.change, Some(MembershipChange::Knocked));
892 } else {
893 panic!("Unexpected state event type");
894 }
895 }
896
897 #[async_test]
898 async fn test_latest_message_includes_bundled_edit() {
899 let room_id = room_id!("!q:x.uk");
902 let user_id = user_id!("@t:o.uk");
903
904 let f = EventFactory::new();
905
906 let original_event_id = event_id!("$original");
907
908 let mut relations = BundledMessageLikeRelations::new();
909 relations.replace = Some(Box::new(
910 f.text_html(" * Updated!", " * <b>Updated!</b>")
911 .edit(
912 original_event_id,
913 MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
914 )
915 .event_id(event_id!("$edit"))
916 .sender(user_id)
917 .into_raw_sync(),
918 ));
919
920 let event = f
921 .text_html("**My M**", "<b>My M</b>")
922 .sender(user_id)
923 .event_id(original_event_id)
924 .bundled_relations(relations)
925 .server_ts(42)
926 .into_event();
927
928 let client = logged_in_client(None).await;
929
930 let timeline_item =
932 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
933 .await
934 .unwrap();
935
936 assert_eq!(timeline_item.sender, user_id);
938 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
939 assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
940 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
941 assert_eq!(txt.body, "Updated!");
942 let formatted = txt.formatted.as_ref().unwrap();
943 assert_eq!(formatted.format, MessageFormat::Html);
944 assert_eq!(formatted.body, "<b>Updated!</b>");
945 } else {
946 panic!("Unexpected message type");
947 }
948 }
949
950 #[async_test]
951 async fn test_latest_poll_includes_bundled_edit() {
952 let room_id = room_id!("!q:x.uk");
955 let user_id = user_id!("@t:o.uk");
956
957 let f = EventFactory::new();
958
959 let original_event_id = event_id!("$original");
960
961 let mut relations = BundledMessageLikeRelations::new();
962 relations.replace = Some(Box::new(
963 f.poll_edit(
964 original_event_id,
965 "It's one banana, Michael, how much could it cost?",
966 vec!["1 dollar", "10 dollars", "100 dollars"],
967 )
968 .event_id(event_id!("$edit"))
969 .sender(user_id)
970 .into_raw_sync(),
971 ));
972
973 let event = f
974 .poll_start(
975 "It's one avocado, Michael, how much could it cost? 10 dollars?",
976 "It's one avocado, Michael, how much could it cost?",
977 vec!["1 dollar", "10 dollars", "100 dollars"],
978 )
979 .event_id(original_event_id)
980 .bundled_relations(relations)
981 .sender(user_id)
982 .into_event();
983
984 let client = logged_in_client(None).await;
985
986 let timeline_item =
988 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
989 .await
990 .unwrap();
991
992 assert_eq!(timeline_item.sender, user_id);
994
995 let poll = timeline_item.content().as_poll().unwrap();
996 assert!(poll.has_been_edited);
997 assert_eq!(
998 poll.start_event_content.poll_start.question.text,
999 "It's one banana, Michael, how much could it cost?"
1000 );
1001 }
1002
1003 #[async_test]
1004 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
1005 ) {
1006 use ruma::owned_mxc_uri;
1010 let room_id = room_id!("!q:x.uk");
1011 let user_id = user_id!("@t:o.uk");
1012 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1013 let client = logged_in_client(None).await;
1014 let mut room = http::response::Room::new();
1015 room.required_state.push(member_event_as_state_event(
1016 room_id,
1017 user_id,
1018 "join",
1019 "Alice Margatroid",
1020 "mxc://e.org/SEs",
1021 ));
1022
1023 let response = response_with_room(room_id, room);
1025 client
1026 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1027 .await
1028 .unwrap();
1029
1030 let timeline_item =
1032 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1033 .await
1034 .unwrap();
1035
1036 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1038 assert_eq!(
1039 profile,
1040 Profile {
1041 display_name: Some("Alice Margatroid".to_owned()),
1042 display_name_ambiguous: false,
1043 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1044 }
1045 );
1046 }
1047
1048 #[async_test]
1049 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
1050 ) {
1051 use ruma::owned_mxc_uri;
1055 let room_id = room_id!("!q:x.uk");
1056 let user_id = user_id!("@t:o.uk");
1057 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1058 let client = logged_in_client(None).await;
1059
1060 let member_event = MinimalStateEvent::Original(
1061 member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")
1062 .deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
1063 .unwrap(),
1064 );
1065
1066 let room = http::response::Room::new();
1067 let response = response_with_room(room_id, room);
1072 client
1073 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1074 .await
1075 .unwrap();
1076
1077 let timeline_item = EventTimelineItem::from_latest_event(
1079 client,
1080 room_id,
1081 LatestEvent::new_with_sender_details(event, Some(member_event), None),
1082 )
1083 .await
1084 .unwrap();
1085
1086 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1088 assert_eq!(
1089 profile,
1090 Profile {
1091 display_name: Some("Alice Margatroid".to_owned()),
1092 display_name_ambiguous: false,
1093 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1094 }
1095 );
1096 }
1097
1098 #[async_test]
1099 async fn test_emoji_detection() {
1100 let room_id = room_id!("!q:x.uk");
1101 let user_id = user_id!("@t:o.uk");
1102 let client = logged_in_client(None).await;
1103
1104 let mut event = message_event(room_id, user_id, "π€·ββοΈ No boost π€·ββοΈ", "", 0);
1105 let mut timeline_item =
1106 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1107 .await
1108 .unwrap();
1109
1110 assert!(!timeline_item.contains_only_emojis());
1111
1112 event = message_event(room_id, user_id, " π ", "", 0);
1114 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, "π¨βπ©βπ¦1οΈβ£ππ³πΎββοΈπͺ©πππ»π«±πΌβπ«²πΎππ", "", 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
1140 fn member_event(
1141 room_id: &RoomId,
1142 user_id: &UserId,
1143 display_name: &str,
1144 avatar_url: &str,
1145 ) -> Raw<AnySyncTimelineEvent> {
1146 sync_timeline_event!({
1147 "type": "m.room.member",
1148 "content": {
1149 "avatar_url": avatar_url,
1150 "displayname": display_name,
1151 "membership": "join",
1152 "reason": ""
1153 },
1154 "event_id": "$143273582443PhrSn:example.org",
1155 "origin_server_ts": 143273583,
1156 "room_id": room_id,
1157 "sender": "@example:example.org",
1158 "state_key": user_id,
1159 "type": "m.room.member",
1160 "unsigned": {
1161 "age": 1234
1162 }
1163 })
1164 }
1165
1166 fn member_event_as_state_event(
1167 room_id: &RoomId,
1168 user_id: &UserId,
1169 membership: &str,
1170 display_name: &str,
1171 avatar_url: &str,
1172 ) -> Raw<AnySyncStateEvent> {
1173 sync_state_event!({
1174 "type": "m.room.member",
1175 "content": {
1176 "avatar_url": avatar_url,
1177 "displayname": display_name,
1178 "membership": membership,
1179 "reason": ""
1180 },
1181 "event_id": "$143273582443PhrSn:example.org",
1182 "origin_server_ts": 143273583,
1183 "room_id": room_id,
1184 "sender": user_id,
1185 "state_key": user_id,
1186 "unsigned": {
1187 "age": 1234
1188 }
1189 })
1190 }
1191
1192 fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
1193 let mut response = http::Response::new("6".to_owned());
1194 response.rooms.insert(room_id.to_owned(), room);
1195 response
1196 }
1197
1198 fn message_event(
1199 room_id: &RoomId,
1200 user_id: &UserId,
1201 body: &str,
1202 formatted_body: &str,
1203 ts: u64,
1204 ) -> TimelineEvent {
1205 TimelineEvent::new(sync_timeline_event!({
1206 "event_id": "$eventid6",
1207 "sender": user_id,
1208 "origin_server_ts": ts,
1209 "type": "m.room.message",
1210 "room_id": room_id.to_string(),
1211 "content": {
1212 "body": body,
1213 "format": "org.matrix.custom.html",
1214 "formatted_body": formatted_body,
1215 "msgtype": "m.text"
1216 },
1217 }))
1218 }
1219}