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 self.content().is_unable_to_decrypt() {
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(_)
599 | MsgLikeKind::Poll(_)
600 | MsgLikeKind::Redacted
601 | MsgLikeKind::UnableToDecrypt(_) => None,
602 },
603 TimelineItemContent::MembershipChange(_)
604 | TimelineItemContent::ProfileChange(_)
605 | TimelineItemContent::OtherState(_)
606 | TimelineItemContent::FailedToParseMessageLike { .. }
607 | TimelineItemContent::FailedToParseState { .. }
608 | TimelineItemContent::CallInvite
609 | TimelineItemContent::CallNotify => None,
610 };
611
612 if let Some(body) = body {
613 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
615
616 if graphemes.len() > 5 {
621 return false;
622 }
623
624 graphemes.iter().all(|g| emojis::get(g).is_some())
625 } else {
626 false
627 }
628 }
629}
630
631impl From<LocalEventTimelineItem> for EventTimelineItemKind {
632 fn from(value: LocalEventTimelineItem) -> Self {
633 EventTimelineItemKind::Local(value)
634 }
635}
636
637impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
638 fn from(value: RemoteEventTimelineItem) -> Self {
639 EventTimelineItemKind::Remote(value)
640 }
641}
642
643#[derive(Clone, Debug, Default, PartialEq, Eq)]
645pub struct Profile {
646 pub display_name: Option<String>,
648
649 pub display_name_ambiguous: bool,
655
656 pub avatar_url: Option<OwnedMxcUri>,
658}
659
660#[derive(Clone, Debug)]
664pub enum TimelineDetails<T> {
665 Unavailable,
668
669 Pending,
671
672 Ready(T),
674
675 Error(Arc<Error>),
677}
678
679impl<T> TimelineDetails<T> {
680 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
681 match value {
682 Some(v) => Self::Ready(v),
683 None => Self::Unavailable,
684 }
685 }
686
687 pub(crate) fn is_unavailable(&self) -> bool {
688 matches!(self, Self::Unavailable)
689 }
690
691 pub fn is_ready(&self) -> bool {
692 matches!(self, Self::Ready(_))
693 }
694}
695
696#[derive(Clone, Copy, Debug)]
698#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
699pub enum EventItemOrigin {
700 Local,
702 Sync,
704 Pagination,
706 Cache,
708}
709
710#[derive(Clone, Debug)]
712pub enum ReactionStatus {
713 LocalToLocal(Option<SendReactionHandle>),
717 LocalToRemote(Option<SendHandle>),
721 RemoteToRemote(OwnedEventId),
725}
726
727#[derive(Clone, Debug)]
729pub struct ReactionInfo {
730 pub timestamp: MilliSecondsSinceUnixEpoch,
731 pub status: ReactionStatus,
733}
734
735#[derive(Debug, Clone, Default)]
740pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
741
742impl Deref for ReactionsByKeyBySender {
743 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
744
745 fn deref(&self) -> &Self::Target {
746 &self.0
747 }
748}
749
750impl DerefMut for ReactionsByKeyBySender {
751 fn deref_mut(&mut self) -> &mut Self::Target {
752 &mut self.0
753 }
754}
755
756impl ReactionsByKeyBySender {
757 pub(crate) fn remove_reaction(
763 &mut self,
764 sender: &UserId,
765 annotation: &str,
766 ) -> Option<ReactionInfo> {
767 if let Some(by_user) = self.0.get_mut(annotation) {
768 if let Some(info) = by_user.swap_remove(sender) {
769 if by_user.is_empty() {
771 self.0.swap_remove(annotation);
772 }
773 return Some(info);
774 }
775 }
776 None
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use assert_matches::assert_matches;
783 use assert_matches2::assert_let;
784 use matrix_sdk::test_utils::logged_in_client;
785 use matrix_sdk_base::{
786 deserialized_responses::TimelineEvent, latest_event::LatestEvent, MinimalStateEvent,
787 OriginalMinimalStateEvent, RequestedRequiredStates,
788 };
789 use matrix_sdk_test::{
790 async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event,
791 };
792 use ruma::{
793 api::client::sync::sync_events::v5 as http,
794 event_id,
795 events::{
796 room::{
797 member::RoomMemberEventContent,
798 message::{MessageFormat, MessageType},
799 },
800 AnySyncStateEvent, AnySyncTimelineEvent, BundledMessageLikeRelations,
801 },
802 room_id,
803 serde::Raw,
804 user_id, RoomId, UInt, UserId,
805 };
806
807 use super::{EventTimelineItem, Profile};
808 use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
809
810 #[async_test]
811 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
812 let room_id = room_id!("!q:x.uk");
815 let user_id = user_id!("@t:o.uk");
816 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
817 let client = logged_in_client(None).await;
818
819 let timeline_item =
821 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
822 .await
823 .unwrap();
824
825 assert_eq!(timeline_item.sender, user_id);
827 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
828 assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
829 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
830 assert_eq!(txt.body, "**My M**");
831 let formatted = txt.formatted.as_ref().unwrap();
832 assert_eq!(formatted.format, MessageFormat::Html);
833 assert_eq!(formatted.body, "<b>My M</b>");
834 } else {
835 panic!("Unexpected message type");
836 }
837 }
838
839 #[async_test]
840 async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
841 let room_id = room_id!("!q:x.uk");
845 let user_id = user_id!("@t:o.uk");
846 let raw_event = member_event_as_state_event(
847 room_id,
848 user_id,
849 "knock",
850 "Alice Margatroid",
851 "mxc://e.org/SEs",
852 );
853 let client = logged_in_client(None).await;
854
855 let power_level_event = sync_state_event!({
858 "type": "m.room.power_levels",
859 "content": {},
860 "event_id": "$143278582443PhrSn:example.org",
861 "origin_server_ts": 143273581,
862 "room_id": room_id,
863 "sender": user_id,
864 "state_key": "",
865 "unsigned": {
866 "age": 1234
867 }
868 });
869 let mut room = http::response::Room::new();
870 room.required_state.push(power_level_event);
871
872 let response = response_with_room(room_id, room);
874 client
875 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
876 .await
877 .unwrap();
878
879 let event = TimelineEvent::new(raw_event.cast());
881 let timeline_item =
882 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
883 .await
884 .unwrap();
885
886 assert_eq!(timeline_item.sender, user_id);
888 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
889 assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
890 if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
891 assert_eq!(change.user_id, user_id);
892 assert_matches!(change.change, Some(MembershipChange::Knocked));
893 } else {
894 panic!("Unexpected state event type");
895 }
896 }
897
898 #[async_test]
899 async fn test_latest_message_includes_bundled_edit() {
900 let room_id = room_id!("!q:x.uk");
903 let user_id = user_id!("@t:o.uk");
904
905 let f = EventFactory::new();
906
907 let original_event_id = event_id!("$original");
908
909 let mut relations = BundledMessageLikeRelations::new();
910 relations.replace = Some(Box::new(
911 f.text_html(" * Updated!", " * <b>Updated!</b>")
912 .edit(
913 original_event_id,
914 MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
915 )
916 .event_id(event_id!("$edit"))
917 .sender(user_id)
918 .into_raw_sync(),
919 ));
920
921 let event = f
922 .text_html("**My M**", "<b>My M</b>")
923 .sender(user_id)
924 .event_id(original_event_id)
925 .bundled_relations(relations)
926 .server_ts(42)
927 .into_event();
928
929 let client = logged_in_client(None).await;
930
931 let timeline_item =
933 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
934 .await
935 .unwrap();
936
937 assert_eq!(timeline_item.sender, user_id);
939 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
940 assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
941 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
942 assert_eq!(txt.body, "Updated!");
943 let formatted = txt.formatted.as_ref().unwrap();
944 assert_eq!(formatted.format, MessageFormat::Html);
945 assert_eq!(formatted.body, "<b>Updated!</b>");
946 } else {
947 panic!("Unexpected message type");
948 }
949 }
950
951 #[async_test]
952 async fn test_latest_poll_includes_bundled_edit() {
953 let room_id = room_id!("!q:x.uk");
956 let user_id = user_id!("@t:o.uk");
957
958 let f = EventFactory::new();
959
960 let original_event_id = event_id!("$original");
961
962 let mut relations = BundledMessageLikeRelations::new();
963 relations.replace = Some(Box::new(
964 f.poll_edit(
965 original_event_id,
966 "It's one banana, Michael, how much could it cost?",
967 vec!["1 dollar", "10 dollars", "100 dollars"],
968 )
969 .event_id(event_id!("$edit"))
970 .sender(user_id)
971 .into_raw_sync(),
972 ));
973
974 let event = f
975 .poll_start(
976 "It's one avocado, Michael, how much could it cost? 10 dollars?",
977 "It's one avocado, Michael, how much could it cost?",
978 vec!["1 dollar", "10 dollars", "100 dollars"],
979 )
980 .event_id(original_event_id)
981 .bundled_relations(relations)
982 .sender(user_id)
983 .into_event();
984
985 let client = logged_in_client(None).await;
986
987 let timeline_item =
989 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
990 .await
991 .unwrap();
992
993 assert_eq!(timeline_item.sender, user_id);
995
996 let poll = timeline_item.content().as_poll().unwrap();
997 assert!(poll.has_been_edited);
998 assert_eq!(
999 poll.start_event_content.poll_start.question.text,
1000 "It's one banana, Michael, how much could it cost?"
1001 );
1002 }
1003
1004 #[async_test]
1005 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
1006 ) {
1007 use ruma::owned_mxc_uri;
1011 let room_id = room_id!("!q:x.uk");
1012 let user_id = user_id!("@t:o.uk");
1013 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1014 let client = logged_in_client(None).await;
1015 let mut room = http::response::Room::new();
1016 room.required_state.push(member_event_as_state_event(
1017 room_id,
1018 user_id,
1019 "join",
1020 "Alice Margatroid",
1021 "mxc://e.org/SEs",
1022 ));
1023
1024 let response = response_with_room(room_id, room);
1026 client
1027 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1028 .await
1029 .unwrap();
1030
1031 let timeline_item =
1033 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1034 .await
1035 .unwrap();
1036
1037 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1039 assert_eq!(
1040 profile,
1041 Profile {
1042 display_name: Some("Alice Margatroid".to_owned()),
1043 display_name_ambiguous: false,
1044 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1045 }
1046 );
1047 }
1048
1049 #[async_test]
1050 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
1051 ) {
1052 use ruma::owned_mxc_uri;
1056 let room_id = room_id!("!q:x.uk");
1057 let user_id = user_id!("@t:o.uk");
1058 let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1059 let client = logged_in_client(None).await;
1060
1061 let member_event = MinimalStateEvent::Original(
1062 member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")
1063 .deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
1064 .unwrap(),
1065 );
1066
1067 let room = http::response::Room::new();
1068 let response = response_with_room(room_id, room);
1073 client
1074 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1075 .await
1076 .unwrap();
1077
1078 let timeline_item = EventTimelineItem::from_latest_event(
1080 client,
1081 room_id,
1082 LatestEvent::new_with_sender_details(event, Some(member_event), None),
1083 )
1084 .await
1085 .unwrap();
1086
1087 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1089 assert_eq!(
1090 profile,
1091 Profile {
1092 display_name: Some("Alice Margatroid".to_owned()),
1093 display_name_ambiguous: false,
1094 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1095 }
1096 );
1097 }
1098
1099 #[async_test]
1100 async fn test_emoji_detection() {
1101 let room_id = room_id!("!q:x.uk");
1102 let user_id = user_id!("@t:o.uk");
1103 let client = logged_in_client(None).await;
1104
1105 let mut event = message_event(room_id, user_id, "🤷♂️ No boost 🤷♂️", "", 0);
1106 let mut timeline_item =
1107 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1108 .await
1109 .unwrap();
1110
1111 assert!(!timeline_item.contains_only_emojis());
1112
1113 event = message_event(room_id, user_id, " 🚀 ", "", 0);
1115 timeline_item =
1116 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1117 .await
1118 .unwrap();
1119
1120 assert!(timeline_item.contains_only_emojis());
1121
1122 event = message_event(room_id, user_id, "👨👩👦1️⃣🚀👳🏾♂️🪩👍👍🏻🫱🏼🫲🏾🙂👋", "", 0);
1124 timeline_item =
1125 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1126 .await
1127 .unwrap();
1128
1129 assert!(!timeline_item.contains_only_emojis());
1130
1131 event = message_event(room_id, user_id, "👨👩👦1️⃣👳🏾♂️👍🏻🫱🏼🫲🏾", "", 0);
1133 timeline_item =
1134 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1135 .await
1136 .unwrap();
1137
1138 assert!(timeline_item.contains_only_emojis());
1139 }
1140
1141 fn member_event(
1142 room_id: &RoomId,
1143 user_id: &UserId,
1144 display_name: &str,
1145 avatar_url: &str,
1146 ) -> Raw<AnySyncTimelineEvent> {
1147 sync_timeline_event!({
1148 "type": "m.room.member",
1149 "content": {
1150 "avatar_url": avatar_url,
1151 "displayname": display_name,
1152 "membership": "join",
1153 "reason": ""
1154 },
1155 "event_id": "$143273582443PhrSn:example.org",
1156 "origin_server_ts": 143273583,
1157 "room_id": room_id,
1158 "sender": "@example:example.org",
1159 "state_key": user_id,
1160 "type": "m.room.member",
1161 "unsigned": {
1162 "age": 1234
1163 }
1164 })
1165 }
1166
1167 fn member_event_as_state_event(
1168 room_id: &RoomId,
1169 user_id: &UserId,
1170 membership: &str,
1171 display_name: &str,
1172 avatar_url: &str,
1173 ) -> Raw<AnySyncStateEvent> {
1174 sync_state_event!({
1175 "type": "m.room.member",
1176 "content": {
1177 "avatar_url": avatar_url,
1178 "displayname": display_name,
1179 "membership": membership,
1180 "reason": ""
1181 },
1182 "event_id": "$143273582443PhrSn:example.org",
1183 "origin_server_ts": 143273583,
1184 "room_id": room_id,
1185 "sender": user_id,
1186 "state_key": user_id,
1187 "unsigned": {
1188 "age": 1234
1189 }
1190 })
1191 }
1192
1193 fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
1194 let mut response = http::Response::new("6".to_owned());
1195 response.rooms.insert(room_id.to_owned(), room);
1196 response
1197 }
1198
1199 fn message_event(
1200 room_id: &RoomId,
1201 user_id: &UserId,
1202 body: &str,
1203 formatted_body: &str,
1204 ts: u64,
1205 ) -> TimelineEvent {
1206 TimelineEvent::new(sync_timeline_event!({
1207 "event_id": "$eventid6",
1208 "sender": user_id,
1209 "origin_server_ts": ts,
1210 "type": "m.room.message",
1211 "room_id": room_id.to_string(),
1212 "content": {
1213 "body": body,
1214 "format": "org.matrix.custom.html",
1215 "formatted_body": formatted_body,
1216 "msgtype": "m.text"
1217 },
1218 }))
1219 }
1220}