1use std::{
16 ops::{Deref, DerefMut},
17 sync::Arc,
18};
19
20use as_variant::as_variant;
21use indexmap::IndexMap;
22use matrix_sdk::{
23 Client, Error,
24 deserialized_responses::{EncryptionInfo, ShieldState},
25 send_queue::{SendHandle, SendReactionHandle},
26};
27use matrix_sdk_base::{
28 deserialized_responses::{SENT_IN_CLEAR, ShieldStateCode},
29 latest_event::LatestEvent,
30};
31use once_cell::sync::Lazy;
32use ruma::{
33 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
34 OwnedUserId, RoomId, TransactionId, UserId,
35 events::{AnySyncTimelineEvent, receipt::Receipt, room::message::MessageType},
36 room_version_rules::RedactionRules,
37 serde::Raw,
38};
39use tracing::warn;
40use unicode_segmentation::UnicodeSegmentation;
41
42mod content;
43mod local;
44mod remote;
45
46pub use self::{
47 content::{
48 AnyOtherFullStateEventContent, EmbeddedEvent, EncryptedMessage, InReplyToDetails,
49 MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind,
50 OtherMessageLike, OtherState, PollResult, PollState, RoomMembershipChange,
51 RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
52 },
53 local::{EventSendState, MediaUploadProgress},
54};
55pub(super) use self::{
56 content::{
57 extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
58 },
59 local::LocalEventTimelineItem,
60 remote::{RemoteEventOrigin, RemoteEventTimelineItem},
61};
62
63#[derive(Clone, Debug)]
69pub struct EventTimelineItem {
70 pub(super) sender: OwnedUserId,
72 pub(super) sender_profile: TimelineDetails<Profile>,
74 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
76 pub(super) content: TimelineItemContent,
78 pub(super) kind: EventTimelineItemKind,
80 pub(super) is_room_encrypted: bool,
84}
85
86#[derive(Clone, Debug)]
87pub(super) enum EventTimelineItemKind {
88 Local(LocalEventTimelineItem),
90 Remote(RemoteEventTimelineItem),
92}
93
94#[derive(Clone, Debug, Eq, Hash, PartialEq)]
96pub enum TimelineEventItemId {
97 TransactionId(OwnedTransactionId),
100 EventId(OwnedEventId),
102}
103
104pub(crate) enum TimelineItemHandle<'a> {
110 Remote(&'a EventId),
111 Local(&'a SendHandle),
112}
113
114impl EventTimelineItem {
115 pub(super) fn new(
116 sender: OwnedUserId,
117 sender_profile: TimelineDetails<Profile>,
118 timestamp: MilliSecondsSinceUnixEpoch,
119 content: TimelineItemContent,
120 kind: EventTimelineItemKind,
121 is_room_encrypted: bool,
122 ) -> Self {
123 Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted }
124 }
125
126 pub async fn from_latest_event(
138 client: Client,
139 room_id: &RoomId,
140 latest_event: LatestEvent,
141 ) -> Option<EventTimelineItem> {
142 use super::traits::RoomDataProvider;
146
147 let raw_sync_event = latest_event.event().raw().clone();
148 let encryption_info = latest_event.event().encryption_info().cloned();
149
150 let Ok(event) = raw_sync_event.deserialize() else {
151 warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!");
152 return None;
153 };
154
155 let timestamp = event.origin_server_ts();
156 let sender = event.sender().to_owned();
157 let event_id = event.event_id().to_owned();
158 let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false);
159
160 let power_levels = if let Some(room) = client.get_room(room_id) {
162 room.power_levels().await.ok()
163 } else {
164 None
165 };
166 let room_power_levels_info = client.user_id().zip(power_levels.as_ref());
167
168 let content =
171 TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?;
172
173 let read_receipts = IndexMap::new();
175
176 let is_highlighted = false;
178
179 let latest_edit_json = None;
182
183 let origin = RemoteEventOrigin::Sync;
185
186 let kind = RemoteEventTimelineItem {
187 event_id,
188 transaction_id: None,
189 read_receipts,
190 is_own,
191 is_highlighted,
192 encryption_info,
193 original_json: Some(raw_sync_event),
194 latest_edit_json,
195 origin,
196 }
197 .into();
198
199 let room = client.get_room(room_id);
200 let sender_profile = if let Some(room) = room {
201 let mut profile = room.profile_from_latest_event(&latest_event);
202
203 if profile.is_none() {
205 profile = room.profile_from_user_id(&sender).await;
206 }
207
208 profile.map(TimelineDetails::Ready).unwrap_or(TimelineDetails::Unavailable)
209 } else {
210 TimelineDetails::Unavailable
211 };
212
213 Some(Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted: false })
214 }
215
216 pub fn is_local_echo(&self) -> bool {
223 matches!(self.kind, EventTimelineItemKind::Local(_))
224 }
225
226 pub fn is_remote_event(&self) -> bool {
234 matches!(self.kind, EventTimelineItemKind::Remote(_))
235 }
236
237 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
239 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
240 }
241
242 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
244 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
245 }
246
247 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
250 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
251 }
252
253 pub fn send_state(&self) -> Option<&EventSendState> {
255 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
256 }
257
258 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
260 match &self.kind {
261 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
262 EventTimelineItemKind::Remote(_) => None,
263 }
264 }
265
266 pub fn identifier(&self) -> TimelineEventItemId {
272 match &self.kind {
273 EventTimelineItemKind::Local(local) => local.identifier(),
274 EventTimelineItemKind::Remote(remote) => {
275 TimelineEventItemId::EventId(remote.event_id.clone())
276 }
277 }
278 }
279
280 pub fn transaction_id(&self) -> Option<&TransactionId> {
285 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
286 }
287
288 pub fn event_id(&self) -> Option<&EventId> {
297 match &self.kind {
298 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
299 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
300 }
301 }
302
303 pub fn sender(&self) -> &UserId {
305 &self.sender
306 }
307
308 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
310 &self.sender_profile
311 }
312
313 pub fn content(&self) -> &TimelineItemContent {
315 &self.content
316 }
317
318 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
320 &mut self.content
321 }
322
323 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
330 static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
331 match &self.kind {
332 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
333 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
334 }
335 }
336
337 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
343 self.timestamp
344 }
345
346 pub fn is_own(&self) -> bool {
348 match &self.kind {
349 EventTimelineItemKind::Local(_) => true,
350 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
351 }
352 }
353
354 pub fn is_editable(&self) -> bool {
356 if !self.is_own() {
360 return false;
362 }
363
364 match self.content() {
365 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
366 MsgLikeKind::Message(message) => match message.msgtype() {
367 MessageType::Text(_)
368 | MessageType::Emote(_)
369 | MessageType::Audio(_)
370 | MessageType::File(_)
371 | MessageType::Image(_)
372 | MessageType::Video(_) => true,
373 #[cfg(feature = "unstable-msc4274")]
374 MessageType::Gallery(_) => true,
375 _ => false,
376 },
377 MsgLikeKind::Poll(poll) => {
378 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
379 }
380 _ => false,
382 },
383 _ => {
384 false
386 }
387 }
388 }
389
390 pub fn is_highlighted(&self) -> bool {
392 match &self.kind {
393 EventTimelineItemKind::Local(_) => false,
394 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
395 }
396 }
397
398 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
400 match &self.kind {
401 EventTimelineItemKind::Local(_) => None,
402 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
403 }
404 }
405
406 pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
409 if !self.is_room_encrypted || self.is_local_echo() {
410 return None;
411 }
412
413 if self.content().is_unable_to_decrypt() {
415 return None;
416 }
417
418 match self.encryption_info() {
419 Some(info) => {
420 if strict {
421 Some(info.verification_state.to_shield_state_strict())
422 } else {
423 Some(info.verification_state.to_shield_state_lax())
424 }
425 }
426 None => Some(ShieldState::Red {
427 code: ShieldStateCode::SentInClear,
428 message: SENT_IN_CLEAR,
429 }),
430 }
431 }
432
433 pub fn can_be_replied_to(&self) -> bool {
435 if self.event_id().is_none() {
437 false
438 } else if self.content.is_message() {
439 true
440 } else {
441 self.latest_json().is_some()
442 }
443 }
444
445 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
451 match &self.kind {
452 EventTimelineItemKind::Local(_) => None,
453 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
454 }
455 }
456
457 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
459 match &self.kind {
460 EventTimelineItemKind::Local(_) => None,
461 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
462 }
463 }
464
465 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
468 self.latest_edit_json().or_else(|| self.original_json())
469 }
470
471 pub fn origin(&self) -> Option<EventItemOrigin> {
475 match &self.kind {
476 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
477 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
478 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
479 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
480 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
481 RemoteEventOrigin::Unknown => None,
482 },
483 }
484 }
485
486 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
487 self.content = content;
488 }
489
490 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
492 Self { kind: kind.into(), ..self.clone() }
493 }
494
495 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
497 let mut new = self.clone();
498 new.content = new_content;
499 new
500 }
501
502 pub(super) fn with_content_and_latest_edit(
507 &self,
508 new_content: TimelineItemContent,
509 edit_json: Option<Raw<AnySyncTimelineEvent>>,
510 ) -> Self {
511 let mut new = self.clone();
512 new.content = new_content;
513 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
514 r.latest_edit_json = edit_json;
515 }
516 new
517 }
518
519 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
521 Self { sender_profile, ..self.clone() }
522 }
523
524 pub(super) fn with_encryption_info(
526 &self,
527 encryption_info: Option<Arc<EncryptionInfo>>,
528 ) -> Self {
529 let mut new = self.clone();
530 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
531 r.encryption_info = encryption_info;
532 }
533
534 new
535 }
536
537 pub(super) fn redact(&self, rules: &RedactionRules) -> Self {
539 let content = self.content.redact(rules);
540 let kind = match &self.kind {
541 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
542 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
543 };
544 Self {
545 sender: self.sender.clone(),
546 sender_profile: self.sender_profile.clone(),
547 timestamp: self.timestamp,
548 content,
549 kind,
550 is_room_encrypted: self.is_room_encrypted,
551 }
552 }
553
554 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
555 match &self.kind {
556 EventTimelineItemKind::Local(local) => {
557 if let Some(event_id) = local.event_id() {
558 TimelineItemHandle::Remote(event_id)
559 } else {
560 TimelineItemHandle::Local(
561 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
563 )
564 }
565 }
566 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
567 }
568 }
569
570 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
572 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
573 }
574
575 pub fn contains_only_emojis(&self) -> bool {
598 let body = match self.content() {
599 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
600 MsgLikeKind::Message(message) => match &message.msgtype {
601 MessageType::Text(text) => Some(text.body.as_str()),
602 MessageType::Audio(audio) => audio.caption(),
603 MessageType::File(file) => file.caption(),
604 MessageType::Image(image) => image.caption(),
605 MessageType::Video(video) => video.caption(),
606 _ => None,
607 },
608 MsgLikeKind::Sticker(_)
609 | MsgLikeKind::Poll(_)
610 | MsgLikeKind::Redacted
611 | MsgLikeKind::UnableToDecrypt(_)
612 | MsgLikeKind::Other(_) => None,
613 },
614 TimelineItemContent::MembershipChange(_)
615 | TimelineItemContent::ProfileChange(_)
616 | TimelineItemContent::OtherState(_)
617 | TimelineItemContent::FailedToParseMessageLike { .. }
618 | TimelineItemContent::FailedToParseState { .. }
619 | TimelineItemContent::CallInvite
620 | TimelineItemContent::RtcNotification => None,
621 };
622
623 if let Some(body) = body {
624 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
626
627 if graphemes.len() > 5 {
632 return false;
633 }
634
635 graphemes.iter().all(|g| emojis::get(g).is_some())
636 } else {
637 false
638 }
639 }
640}
641
642impl From<LocalEventTimelineItem> for EventTimelineItemKind {
643 fn from(value: LocalEventTimelineItem) -> Self {
644 EventTimelineItemKind::Local(value)
645 }
646}
647
648impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
649 fn from(value: RemoteEventTimelineItem) -> Self {
650 EventTimelineItemKind::Remote(value)
651 }
652}
653
654#[derive(Clone, Debug, Default, PartialEq, Eq)]
656pub struct Profile {
657 pub display_name: Option<String>,
659
660 pub display_name_ambiguous: bool,
666
667 pub avatar_url: Option<OwnedMxcUri>,
669}
670
671#[derive(Clone, Debug)]
675pub enum TimelineDetails<T> {
676 Unavailable,
679
680 Pending,
682
683 Ready(T),
685
686 Error(Arc<Error>),
688}
689
690impl<T> TimelineDetails<T> {
691 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
692 match value {
693 Some(v) => Self::Ready(v),
694 None => Self::Unavailable,
695 }
696 }
697
698 pub fn is_unavailable(&self) -> bool {
699 matches!(self, Self::Unavailable)
700 }
701
702 pub fn is_ready(&self) -> bool {
703 matches!(self, Self::Ready(_))
704 }
705}
706
707#[derive(Clone, Copy, Debug)]
709#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
710pub enum EventItemOrigin {
711 Local,
713 Sync,
715 Pagination,
717 Cache,
719}
720
721#[derive(Clone, Debug)]
723pub enum ReactionStatus {
724 LocalToLocal(Option<SendReactionHandle>),
728 LocalToRemote(Option<SendHandle>),
732 RemoteToRemote(OwnedEventId),
736}
737
738#[derive(Clone, Debug)]
740pub struct ReactionInfo {
741 pub timestamp: MilliSecondsSinceUnixEpoch,
742 pub status: ReactionStatus,
744}
745
746#[derive(Debug, Clone, Default)]
751pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
752
753impl Deref for ReactionsByKeyBySender {
754 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
755
756 fn deref(&self) -> &Self::Target {
757 &self.0
758 }
759}
760
761impl DerefMut for ReactionsByKeyBySender {
762 fn deref_mut(&mut self) -> &mut Self::Target {
763 &mut self.0
764 }
765}
766
767impl ReactionsByKeyBySender {
768 pub(crate) fn remove_reaction(
774 &mut self,
775 sender: &UserId,
776 annotation: &str,
777 ) -> Option<ReactionInfo> {
778 if let Some(by_user) = self.0.get_mut(annotation)
779 && let Some(info) = by_user.swap_remove(sender)
780 {
781 if by_user.is_empty() {
783 self.0.swap_remove(annotation);
784 }
785 return Some(info);
786 }
787 None
788 }
789}
790
791#[cfg(test)]
792mod tests {
793 use assert_matches::assert_matches;
794 use assert_matches2::assert_let;
795 use matrix_sdk::test_utils::logged_in_client;
796 use matrix_sdk_base::{
797 MinimalStateEvent, OriginalMinimalStateEvent, RequestedRequiredStates,
798 deserialized_responses::TimelineEvent, latest_event::LatestEvent,
799 };
800 use matrix_sdk_test::{async_test, event_factory::EventFactory, sync_state_event};
801 use ruma::{
802 RoomId, UInt, UserId,
803 api::client::sync::sync_events::v5 as http,
804 event_id,
805 events::{
806 AnySyncStateEvent,
807 room::{
808 member::RoomMemberEventContent,
809 message::{MessageFormat, MessageType},
810 },
811 },
812 room_id,
813 serde::Raw,
814 user_id,
815 };
816
817 use super::{EventTimelineItem, Profile};
818 use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
819
820 #[async_test]
821 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
822 let room_id = room_id!("!q:x.uk");
825 let user_id = user_id!("@t:o.uk");
826 let event = EventFactory::new()
827 .room(room_id)
828 .text_html("**My M**", "<b>My M</b>")
829 .sender(user_id)
830 .server_ts(122344)
831 .into_event();
832 let client = logged_in_client(None).await;
833
834 let timeline_item =
836 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
837 .await
838 .unwrap();
839
840 assert_eq!(timeline_item.sender, user_id);
842 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
843 assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
844 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
845 assert_eq!(txt.body, "**My M**");
846 let formatted = txt.formatted.as_ref().unwrap();
847 assert_eq!(formatted.format, MessageFormat::Html);
848 assert_eq!(formatted.body, "<b>My M</b>");
849 } else {
850 panic!("Unexpected message type");
851 }
852 }
853
854 #[async_test]
855 async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
856 let room_id = room_id!("!q:x.uk");
860 let user_id = user_id!("@t:o.uk");
861 let raw_event = member_event_as_state_event(
862 room_id,
863 user_id,
864 "knock",
865 "Alice Margatroid",
866 "mxc://e.org/SEs",
867 );
868 let client = logged_in_client(None).await;
869
870 let create_event = sync_state_event!({
873 "type": "m.room.create",
874 "content": { "room_version": "11" },
875 "event_id": "$143278582443PhrSm:example.org",
876 "origin_server_ts": 143273580,
877 "room_id": room_id,
878 "sender": user_id,
879 "state_key": "",
880 "unsigned": {
881 "age": 1235
882 }
883 });
884 let power_level_event = sync_state_event!({
885 "type": "m.room.power_levels",
886 "content": {},
887 "event_id": "$143278582443PhrSn:example.org",
888 "origin_server_ts": 143273581,
889 "room_id": room_id,
890 "sender": user_id,
891 "state_key": "",
892 "unsigned": {
893 "age": 1234
894 }
895 });
896 let mut room = http::response::Room::new();
897 room.required_state.extend([create_event, power_level_event]);
898
899 let response = response_with_room(room_id, room);
901 client
902 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
903 .await
904 .unwrap();
905
906 let event = TimelineEvent::from_plaintext(raw_event.cast());
908 let timeline_item =
909 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
910 .await
911 .unwrap();
912
913 assert_eq!(timeline_item.sender, user_id);
915 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
916 assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
917 if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
918 assert_eq!(change.user_id, user_id);
919 assert_matches!(change.change, Some(MembershipChange::Knocked));
920 } else {
921 panic!("Unexpected state event type");
922 }
923 }
924
925 #[async_test]
926 async fn test_latest_message_includes_bundled_edit() {
927 let room_id = room_id!("!q:x.uk");
930 let user_id = user_id!("@t:o.uk");
931
932 let f = EventFactory::new();
933
934 let original_event_id = event_id!("$original");
935
936 let event = f
937 .text_html("**My M**", "<b>My M</b>")
938 .sender(user_id)
939 .event_id(original_event_id)
940 .with_bundled_edit(
941 f.text_html(" * Updated!", " * <b>Updated!</b>")
942 .edit(
943 original_event_id,
944 MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
945 )
946 .event_id(event_id!("$edit"))
947 .sender(user_id),
948 )
949 .server_ts(42)
950 .into_event();
951
952 let client = logged_in_client(None).await;
953
954 let timeline_item =
956 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
957 .await
958 .unwrap();
959
960 assert_eq!(timeline_item.sender, user_id);
962 assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
963 assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
964 if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
965 assert_eq!(txt.body, "Updated!");
966 let formatted = txt.formatted.as_ref().unwrap();
967 assert_eq!(formatted.format, MessageFormat::Html);
968 assert_eq!(formatted.body, "<b>Updated!</b>");
969 } else {
970 panic!("Unexpected message type");
971 }
972 }
973
974 #[async_test]
975 async fn test_latest_poll_includes_bundled_edit() {
976 let room_id = room_id!("!q:x.uk");
979 let user_id = user_id!("@t:o.uk");
980
981 let f = EventFactory::new();
982
983 let original_event_id = event_id!("$original");
984
985 let event = f
986 .poll_start(
987 "It's one avocado, Michael, how much could it cost? 10 dollars?",
988 "It's one avocado, Michael, how much could it cost?",
989 vec!["1 dollar", "10 dollars", "100 dollars"],
990 )
991 .event_id(original_event_id)
992 .with_bundled_edit(
993 f.poll_edit(
994 original_event_id,
995 "It's one banana, Michael, how much could it cost?",
996 vec!["1 dollar", "10 dollars", "100 dollars"],
997 )
998 .event_id(event_id!("$edit"))
999 .sender(user_id),
1000 )
1001 .sender(user_id)
1002 .into_event();
1003
1004 let client = logged_in_client(None).await;
1005
1006 let timeline_item =
1008 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1009 .await
1010 .unwrap();
1011
1012 assert_eq!(timeline_item.sender, user_id);
1014
1015 let poll = timeline_item.content().as_poll().unwrap();
1016 assert!(poll.has_been_edited);
1017 assert_eq!(
1018 poll.poll_start.question.text,
1019 "It's one banana, Michael, how much could it cost?"
1020 );
1021 }
1022
1023 #[async_test]
1024 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage()
1025 {
1026 use ruma::owned_mxc_uri;
1030 let room_id = room_id!("!q:x.uk");
1031 let user_id = user_id!("@t:o.uk");
1032 let event = EventFactory::new()
1033 .room(room_id)
1034 .text_html("**My M**", "<b>My M</b>")
1035 .sender(user_id)
1036 .into_event();
1037 let client = logged_in_client(None).await;
1038 let mut room = http::response::Room::new();
1039 room.required_state.push(member_event_as_state_event(
1040 room_id,
1041 user_id,
1042 "join",
1043 "Alice Margatroid",
1044 "mxc://e.org/SEs",
1045 ));
1046
1047 let response = response_with_room(room_id, room);
1049 client
1050 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1051 .await
1052 .unwrap();
1053
1054 let timeline_item =
1056 EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1057 .await
1058 .unwrap();
1059
1060 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1062 assert_eq!(
1063 profile,
1064 Profile {
1065 display_name: Some("Alice Margatroid".to_owned()),
1066 display_name_ambiguous: false,
1067 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1068 }
1069 );
1070 }
1071
1072 #[async_test]
1073 async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache()
1074 {
1075 use ruma::owned_mxc_uri;
1079 let room_id = room_id!("!q:x.uk");
1080 let user_id = user_id!("@t:o.uk");
1081 let f = EventFactory::new().room(room_id);
1082 let event = f.text_html("**My M**", "<b>My M</b>").sender(user_id).into_event();
1083 let client = logged_in_client(None).await;
1084
1085 let member_event = MinimalStateEvent::Original(
1086 f.member(user_id)
1087 .sender(user_id!("@example:example.org"))
1088 .avatar_url("mxc://e.org/SEs".into())
1089 .display_name("Alice Margatroid")
1090 .reason("")
1091 .into_raw_sync()
1092 .deserialize_as_unchecked::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
1093 .unwrap(),
1094 );
1095
1096 let room = http::response::Room::new();
1097 let response = response_with_room(room_id, room);
1102 client
1103 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1104 .await
1105 .unwrap();
1106
1107 let timeline_item = EventTimelineItem::from_latest_event(
1109 client,
1110 room_id,
1111 LatestEvent::new_with_sender_details(event, Some(member_event), None),
1112 )
1113 .await
1114 .unwrap();
1115
1116 assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1118 assert_eq!(
1119 profile,
1120 Profile {
1121 display_name: Some("Alice Margatroid".to_owned()),
1122 display_name_ambiguous: false,
1123 avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1124 }
1125 );
1126 }
1127
1128 #[async_test]
1129 async fn test_emoji_detection() {
1130 let room_id = room_id!("!q:x.uk");
1131 let user_id = user_id!("@t:o.uk");
1132 let client = logged_in_client(None).await;
1133 let f = EventFactory::new().room(room_id).sender(user_id);
1134
1135 let mut event = f.text_html("π€·ββοΈ No boost π€·ββοΈ", "").into_event();
1136 let mut timeline_item =
1137 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1138 .await
1139 .unwrap();
1140
1141 assert!(!timeline_item.contains_only_emojis());
1142
1143 event = f.text_html(" π ", "").into_event();
1145 timeline_item =
1146 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1147 .await
1148 .unwrap();
1149
1150 assert!(timeline_item.contains_only_emojis());
1151
1152 event = f.text_html("π¨βπ©βπ¦1οΈβ£ππ³πΎββοΈπͺ©πππ»π«±πΌβπ«²πΎππ", "").into_event();
1154 timeline_item =
1155 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1156 .await
1157 .unwrap();
1158
1159 assert!(!timeline_item.contains_only_emojis());
1160
1161 event = f.text_html("π¨βπ©βπ¦1οΈβ£π³πΎββοΈππ»π«±πΌβπ«²πΎ", "").into_event();
1163 timeline_item =
1164 EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1165 .await
1166 .unwrap();
1167
1168 assert!(timeline_item.contains_only_emojis());
1169 }
1170
1171 fn member_event_as_state_event(
1172 room_id: &RoomId,
1173 user_id: &UserId,
1174 membership: &str,
1175 display_name: &str,
1176 avatar_url: &str,
1177 ) -> Raw<AnySyncStateEvent> {
1178 sync_state_event!({
1179 "type": "m.room.member",
1180 "content": {
1181 "avatar_url": avatar_url,
1182 "displayname": display_name,
1183 "membership": membership,
1184 "reason": ""
1185 },
1186 "event_id": "$143273582443PhrSn:example.org",
1187 "origin_server_ts": 143273583,
1188 "room_id": room_id,
1189 "sender": user_id,
1190 "state_key": user_id,
1191 "unsigned": {
1192 "age": 1234
1193 }
1194 })
1195 }
1196
1197 fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
1198 let mut response = http::Response::new("6".to_owned());
1199 response.rooms.insert(room_id.to_owned(), room);
1200 response
1201 }
1202}