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, EmbeddedEvent, EncryptedMessage, InReplyToDetails,
55 MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, OtherState,
56 PollResult, PollState, RoomMembershipChange, RoomPinnedEventsChange, Sticker,
57 ThreadSummary, TimelineItemContent,
58 },
59 local::EventSendState,
60};
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(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
319 &mut self.content
320 }
321
322 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
329 static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
330 match &self.kind {
331 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
332 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
333 }
334 }
335
336 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
342 self.timestamp
343 }
344
345 pub fn is_own(&self) -> bool {
347 match &self.kind {
348 EventTimelineItemKind::Local(_) => true,
349 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
350 }
351 }
352
353 pub fn is_editable(&self) -> bool {
355 if !self.is_own() {
359 return false;
361 }
362
363 match self.content() {
364 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
365 MsgLikeKind::Message(message) => match message.msgtype() {
366 MessageType::Text(_)
367 | MessageType::Emote(_)
368 | MessageType::Audio(_)
369 | MessageType::File(_)
370 | MessageType::Image(_)
371 | MessageType::Video(_) => true,
372 #[cfg(feature = "unstable-msc4274")]
373 MessageType::Gallery(_) => true,
374 _ => false,
375 },
376 MsgLikeKind::Poll(poll) => {
377 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
378 }
379 _ => false,
381 },
382 _ => {
383 false
385 }
386 }
387 }
388
389 pub fn is_highlighted(&self) -> bool {
391 match &self.kind {
392 EventTimelineItemKind::Local(_) => false,
393 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
394 }
395 }
396
397 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
399 match &self.kind {
400 EventTimelineItemKind::Local(_) => None,
401 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
402 }
403 }
404
405 pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
408 if !self.is_room_encrypted || self.is_local_echo() {
409 return None;
410 }
411
412 if self.content().is_unable_to_decrypt() {
414 return None;
415 }
416
417 match self.encryption_info() {
418 Some(info) => {
419 if strict {
420 Some(info.verification_state.to_shield_state_strict())
421 } else {
422 Some(info.verification_state.to_shield_state_lax())
423 }
424 }
425 None => Some(ShieldState::Red {
426 code: ShieldStateCode::SentInClear,
427 message: SENT_IN_CLEAR,
428 }),
429 }
430 }
431
432 pub fn can_be_replied_to(&self) -> bool {
434 if self.event_id().is_none() {
436 false
437 } else if self.content.is_message() {
438 true
439 } else {
440 self.latest_json().is_some()
441 }
442 }
443
444 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
450 match &self.kind {
451 EventTimelineItemKind::Local(_) => None,
452 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
453 }
454 }
455
456 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
458 match &self.kind {
459 EventTimelineItemKind::Local(_) => None,
460 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
461 }
462 }
463
464 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
467 self.latest_edit_json().or_else(|| self.original_json())
468 }
469
470 pub fn origin(&self) -> Option<EventItemOrigin> {
474 match &self.kind {
475 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
476 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
477 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
478 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
479 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
480 RemoteEventOrigin::Unknown => None,
481 },
482 }
483 }
484
485 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
486 self.content = content;
487 }
488
489 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
491 Self { kind: kind.into(), ..self.clone() }
492 }
493
494 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
496 let mut new = self.clone();
497 new.content = new_content;
498 new
499 }
500
501 pub(super) fn with_content_and_latest_edit(
506 &self,
507 new_content: TimelineItemContent,
508 edit_json: Option<Raw<AnySyncTimelineEvent>>,
509 ) -> Self {
510 let mut new = self.clone();
511 new.content = new_content;
512 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
513 r.latest_edit_json = edit_json;
514 }
515 new
516 }
517
518 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
520 Self { sender_profile, ..self.clone() }
521 }
522
523 pub(super) fn with_encryption_info(
525 &self,
526 encryption_info: Option<Arc<EncryptionInfo>>,
527 ) -> Self {
528 let mut new = self.clone();
529 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
530 r.encryption_info = encryption_info;
531 }
532
533 new
534 }
535
536 pub(super) fn redact(&self, room_version: &RoomVersionId) -> Self {
538 let content = self.content.redact(room_version);
539 let kind = match &self.kind {
540 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
541 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
542 };
543 Self {
544 sender: self.sender.clone(),
545 sender_profile: self.sender_profile.clone(),
546 timestamp: self.timestamp,
547 content,
548 kind,
549 is_room_encrypted: self.is_room_encrypted,
550 }
551 }
552
553 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
554 match &self.kind {
555 EventTimelineItemKind::Local(local) => {
556 if let Some(event_id) = local.event_id() {
557 TimelineItemHandle::Remote(event_id)
558 } else {
559 TimelineItemHandle::Local(
560 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
562 )
563 }
564 }
565 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
566 }
567 }
568
569 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
571 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
572 }
573
574 pub fn contains_only_emojis(&self) -> bool {
597 let body = match self.content() {
598 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
599 MsgLikeKind::Message(message) => match &message.msgtype {
600 MessageType::Text(text) => Some(text.body.as_str()),
601 MessageType::Audio(audio) => audio.caption(),
602 MessageType::File(file) => file.caption(),
603 MessageType::Image(image) => image.caption(),
604 MessageType::Video(video) => video.caption(),
605 _ => None,
606 },
607 MsgLikeKind::Sticker(_)
608 | MsgLikeKind::Poll(_)
609 | MsgLikeKind::Redacted
610 | MsgLikeKind::UnableToDecrypt(_) => None,
611 },
612 TimelineItemContent::MembershipChange(_)
613 | TimelineItemContent::ProfileChange(_)
614 | TimelineItemContent::OtherState(_)
615 | TimelineItemContent::FailedToParseMessageLike { .. }
616 | TimelineItemContent::FailedToParseState { .. }
617 | TimelineItemContent::CallInvite
618 | TimelineItemContent::CallNotify => None,
619 };
620
621 if let Some(body) = body {
622 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
624
625 if graphemes.len() > 5 {
630 return false;
631 }
632
633 graphemes.iter().all(|g| emojis::get(g).is_some())
634 } else {
635 false
636 }
637 }
638}
639
640impl From<LocalEventTimelineItem> for EventTimelineItemKind {
641 fn from(value: LocalEventTimelineItem) -> Self {
642 EventTimelineItemKind::Local(value)
643 }
644}
645
646impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
647 fn from(value: RemoteEventTimelineItem) -> Self {
648 EventTimelineItemKind::Remote(value)
649 }
650}
651
652#[derive(Clone, Debug, Default, PartialEq, Eq)]
654pub struct Profile {
655 pub display_name: Option<String>,
657
658 pub display_name_ambiguous: bool,
664
665 pub avatar_url: Option<OwnedMxcUri>,
667}
668
669#[derive(Clone, Debug)]
673pub enum TimelineDetails<T> {
674 Unavailable,
677
678 Pending,
680
681 Ready(T),
683
684 Error(Arc<Error>),
686}
687
688impl<T> TimelineDetails<T> {
689 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
690 match value {
691 Some(v) => Self::Ready(v),
692 None => Self::Unavailable,
693 }
694 }
695
696 pub fn is_unavailable(&self) -> bool {
697 matches!(self, Self::Unavailable)
698 }
699
700 pub fn is_ready(&self) -> bool {
701 matches!(self, Self::Ready(_))
702 }
703}
704
705#[derive(Clone, Copy, Debug)]
707#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
708pub enum EventItemOrigin {
709 Local,
711 Sync,
713 Pagination,
715 Cache,
717}
718
719#[derive(Clone, Debug)]
721pub enum ReactionStatus {
722 LocalToLocal(Option<SendReactionHandle>),
726 LocalToRemote(Option<SendHandle>),
730 RemoteToRemote(OwnedEventId),
734}
735
736#[derive(Clone, Debug)]
738pub struct ReactionInfo {
739 pub timestamp: MilliSecondsSinceUnixEpoch,
740 pub status: ReactionStatus,
742}
743
744#[derive(Debug, Clone, Default)]
749pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
750
751impl Deref for ReactionsByKeyBySender {
752 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
753
754 fn deref(&self) -> &Self::Target {
755 &self.0
756 }
757}
758
759impl DerefMut for ReactionsByKeyBySender {
760 fn deref_mut(&mut self) -> &mut Self::Target {
761 &mut self.0
762 }
763}
764
765impl ReactionsByKeyBySender {
766 pub(crate) fn remove_reaction(
772 &mut self,
773 sender: &UserId,
774 annotation: &str,
775 ) -> Option<ReactionInfo> {
776 if let Some(by_user) = self.0.get_mut(annotation) {
777 if let Some(info) = by_user.swap_remove(sender) {
778 if by_user.is_empty() {
780 self.0.swap_remove(annotation);
781 }
782 return Some(info);
783 }
784 }
785 None
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use assert_matches::assert_matches;
792 use assert_matches2::assert_let;
793 use matrix_sdk::test_utils::logged_in_client;
794 use matrix_sdk_base::{
795 deserialized_responses::TimelineEvent, latest_event::LatestEvent, MinimalStateEvent,
796 OriginalMinimalStateEvent, RequestedRequiredStates,
797 };
798 use matrix_sdk_test::{async_test, event_factory::EventFactory, sync_state_event};
799 use ruma::{
800 api::client::sync::sync_events::v5 as http,
801 event_id,
802 events::{
803 room::{
804 member::RoomMemberEventContent,
805 message::{MessageFormat, MessageType},
806 },
807 AnySyncStateEvent,
808 },
809 room_id,
810 serde::Raw,
811 user_id, RoomId, UInt, UserId,
812 };
813
814 use super::{EventTimelineItem, Profile};
815 use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
816
817 #[async_test]
818 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
819 let room_id = room_id!("!q:x.uk");
822 let user_id = user_id!("@t:o.uk");
823 let event = EventFactory::new()
824 .room(room_id)
825 .text_html("**My M**", "<b>My M</b>")
826 .sender(user_id)
827 .server_ts(122344)
828 .into_event();
829 let client = logged_in_client(None).await;
830
831 let timeline_item =
833 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
834 .await
835 .unwrap();
836
837 assert_eq!(timeline_item.sender, user_id);
839 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
840 assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
841 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
842 assert_eq!(txt.body, "**My M**");
843 let formatted = txt.formatted.as_ref().unwrap();
844 assert_eq!(formatted.format, MessageFormat::Html);
845 assert_eq!(formatted.body, "<b>My M</b>");
846 } else {
847 panic!("Unexpected message type");
848 }
849 }
850
851 #[async_test]
852 async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
853 let room_id = room_id!("!q:x.uk");
857 let user_id = user_id!("@t:o.uk");
858 let raw_event = member_event_as_state_event(
859 room_id,
860 user_id,
861 "knock",
862 "Alice Margatroid",
863 "mxc://e.org/SEs",
864 );
865 let client = logged_in_client(None).await;
866
867 let power_level_event = sync_state_event!({
870 "type": "m.room.power_levels",
871 "content": {},
872 "event_id": "$143278582443PhrSn:example.org",
873 "origin_server_ts": 143273581,
874 "room_id": room_id,
875 "sender": user_id,
876 "state_key": "",
877 "unsigned": {
878 "age": 1234
879 }
880 });
881 let mut room = http::response::Room::new();
882 room.required_state.push(power_level_event);
883
884 let response = response_with_room(room_id, room);
886 client
887 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
888 .await
889 .unwrap();
890
891 let event = TimelineEvent::from_plaintext(raw_event.cast());
893 let timeline_item =
894 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
895 .await
896 .unwrap();
897
898 assert_eq!(timeline_item.sender, user_id);
900 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
901 assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
902 if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
903 assert_eq!(change.user_id, user_id);
904 assert_matches!(change.change, Some(MembershipChange::Knocked));
905 } else {
906 panic!("Unexpected state event type");
907 }
908 }
909
910 #[async_test]
911 async fn test_latest_message_includes_bundled_edit() {
912 let room_id = room_id!("!q:x.uk");
915 let user_id = user_id!("@t:o.uk");
916
917 let f = EventFactory::new();
918
919 let original_event_id = event_id!("$original");
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 .with_bundled_edit(
926 f.text_html(" * Updated!", " * <b>Updated!</b>")
927 .edit(
928 original_event_id,
929 MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
930 )
931 .event_id(event_id!("$edit"))
932 .sender(user_id),
933 )
934 .server_ts(42)
935 .into_event();
936
937 let client = logged_in_client(None).await;
938
939 let timeline_item =
941 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
942 .await
943 .unwrap();
944
945 assert_eq!(timeline_item.sender, user_id);
947 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
948 assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
949 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
950 assert_eq!(txt.body, "Updated!");
951 let formatted = txt.formatted.as_ref().unwrap();
952 assert_eq!(formatted.format, MessageFormat::Html);
953 assert_eq!(formatted.body, "<b>Updated!</b>");
954 } else {
955 panic!("Unexpected message type");
956 }
957 }
958
959 #[async_test]
960 async fn test_latest_poll_includes_bundled_edit() {
961 let room_id = room_id!("!q:x.uk");
964 let user_id = user_id!("@t:o.uk");
965
966 let f = EventFactory::new();
967
968 let original_event_id = event_id!("$original");
969
970 let event = f
971 .poll_start(
972 "It's one avocado, Michael, how much could it cost? 10 dollars?",
973 "It's one avocado, Michael, how much could it cost?",
974 vec!["1 dollar", "10 dollars", "100 dollars"],
975 )
976 .event_id(original_event_id)
977 .with_bundled_edit(
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 )
986 .sender(user_id)
987 .into_event();
988
989 let client = logged_in_client(None).await;
990
991 let timeline_item =
993 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
994 .await
995 .unwrap();
996
997 assert_eq!(timeline_item.sender, user_id);
999
1000 let poll = timeline_item.content().as_poll().unwrap();
1001 assert!(poll.has_been_edited);
1002 assert_eq!(
1003 poll.start_event_content.poll_start.question.text,
1004 "It's one banana, Michael, how much could it cost?"
1005 );
1006 }
1007
1008 #[async_test]
1009 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
1010 ) {
1011 use ruma::owned_mxc_uri;
1015 let room_id = room_id!("!q:x.uk");
1016 let user_id = user_id!("@t:o.uk");
1017 let event = EventFactory::new()
1018 .room(room_id)
1019 .text_html("**My M**", "<b>My M</b>")
1020 .sender(user_id)
1021 .into_event();
1022 let client = logged_in_client(None).await;
1023 let mut room = http::response::Room::new();
1024 room.required_state.push(member_event_as_state_event(
1025 room_id,
1026 user_id,
1027 "join",
1028 "Alice Margatroid",
1029 "mxc://e.org/SEs",
1030 ));
1031
1032 let response = response_with_room(room_id, room);
1034 client
1035 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1036 .await
1037 .unwrap();
1038
1039 let timeline_item =
1041 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1042 .await
1043 .unwrap();
1044
1045 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1047 assert_eq!(
1048 profile,
1049 Profile {
1050 display_name: Some("Alice Margatroid".to_owned()),
1051 display_name_ambiguous: false,
1052 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1053 }
1054 );
1055 }
1056
1057 #[async_test]
1058 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
1059 ) {
1060 use ruma::owned_mxc_uri;
1064 let room_id = room_id!("!q:x.uk");
1065 let user_id = user_id!("@t:o.uk");
1066 let f = EventFactory::new().room(room_id);
1067 let event = f.text_html("**My M**", "<b>My M</b>").sender(user_id).into_event();
1068 let client = logged_in_client(None).await;
1069
1070 let member_event = MinimalStateEvent::Original(
1071 f.member(user_id)
1072 .sender(user_id!("@example:example.org"))
1073 .avatar_url("mxc://e.org/SEs".into())
1074 .display_name("Alice Margatroid")
1075 .reason("")
1076 .into_raw_sync()
1077 .deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
1078 .unwrap(),
1079 );
1080
1081 let room = http::response::Room::new();
1082 let response = response_with_room(room_id, room);
1087 client
1088 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1089 .await
1090 .unwrap();
1091
1092 let timeline_item = EventTimelineItem::from_latest_event(
1094 client,
1095 room_id,
1096 LatestEvent::new_with_sender_details(event, Some(member_event), None),
1097 )
1098 .await
1099 .unwrap();
1100
1101 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1103 assert_eq!(
1104 profile,
1105 Profile {
1106 display_name: Some("Alice Margatroid".to_owned()),
1107 display_name_ambiguous: false,
1108 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1109 }
1110 );
1111 }
1112
1113 #[async_test]
1114 async fn test_emoji_detection() {
1115 let room_id = room_id!("!q:x.uk");
1116 let user_id = user_id!("@t:o.uk");
1117 let client = logged_in_client(None).await;
1118 let f = EventFactory::new().room(room_id).sender(user_id);
1119
1120 let mut event = f.text_html("π€·ββοΈ No boost π€·ββοΈ", "").into_event();
1121 let mut timeline_item =
1122 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1123 .await
1124 .unwrap();
1125
1126 assert!(!timeline_item.contains_only_emojis());
1127
1128 event = f.text_html(" π ", "").into_event();
1130 timeline_item =
1131 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1132 .await
1133 .unwrap();
1134
1135 assert!(timeline_item.contains_only_emojis());
1136
1137 event = f.text_html("π¨βπ©βπ¦1οΈβ£ππ³πΎββοΈπͺ©πππ»π«±πΌβπ«²πΎππ", "").into_event();
1139 timeline_item =
1140 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1141 .await
1142 .unwrap();
1143
1144 assert!(!timeline_item.contains_only_emojis());
1145
1146 event = f.text_html("π¨βπ©βπ¦1οΈβ£π³πΎββοΈππ»π«±πΌβπ«²πΎ", "").into_event();
1148 timeline_item =
1149 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1150 .await
1151 .unwrap();
1152
1153 assert!(timeline_item.contains_only_emojis());
1154 }
1155
1156 fn member_event_as_state_event(
1157 room_id: &RoomId,
1158 user_id: &UserId,
1159 membership: &str,
1160 display_name: &str,
1161 avatar_url: &str,
1162 ) -> Raw<AnySyncStateEvent> {
1163 sync_state_event!({
1164 "type": "m.room.member",
1165 "content": {
1166 "avatar_url": avatar_url,
1167 "displayname": display_name,
1168 "membership": membership,
1169 "reason": ""
1170 },
1171 "event_id": "$143273582443PhrSn:example.org",
1172 "origin_server_ts": 143273583,
1173 "room_id": room_id,
1174 "sender": user_id,
1175 "state_key": user_id,
1176 "unsigned": {
1177 "age": 1234
1178 }
1179 })
1180 }
1181
1182 fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
1183 let mut response = http::Response::new("6".to_owned());
1184 response.rooms.insert(room_id.to_owned(), room);
1185 response
1186 }
1187}