1use std::{
16 ops::{Deref, DerefMut},
17 sync::{Arc, LazyLock},
18};
19
20use as_variant::as_variant;
21use indexmap::IndexMap;
22use matrix_sdk::{
23 Error, Room,
24 deserialized_responses::{EncryptionInfo, ShieldState},
25 send_queue::SendHandle,
26};
27use matrix_sdk_base::deserialized_responses::ShieldStateCode;
28#[cfg(feature = "unstable-msc4426")]
29use ruma::profile::{CallProfileField, StatusProfileField};
30use ruma::{
31 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
32 OwnedUserId, TransactionId, UserId,
33 events::{AnySyncTimelineEvent, receipt::Receipt, room::message::MessageType},
34 room_version_rules::RedactionRules,
35 serde::Raw,
36};
37use tracing::error;
38use unicode_segmentation::UnicodeSegmentation;
39
40mod content;
41mod local;
42mod remote;
43
44pub use self::{
45 content::{
46 AnyOtherStateEventContentChange, BeaconInfo, EmbeddedEvent, EncryptedMessage,
47 InReplyToDetails, LiveLocationState, MemberProfileChange, MembershipChange, Message,
48 MsgLikeContent, MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState,
49 RoomMembershipChange, RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
50 },
51 local::{EventSendState, MediaUploadProgress},
52};
53pub(super) use self::{
54 content::{
55 beacon_info_matches, extract_bundled_edit_event_json, extract_poll_edit_content,
56 extract_room_msg_edit_content,
57 },
58 local::LocalEventTimelineItem,
59 remote::{RemoteEventOrigin, RemoteEventTimelineItem},
60};
61
62#[derive(Clone, Debug)]
68pub struct EventTimelineItem {
69 pub(super) sender: OwnedUserId,
71 pub(super) sender_profile: TimelineDetails<Profile>,
73 pub(super) forwarder: Option<OwnedUserId>,
78 pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
83 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
85 pub(super) content: TimelineItemContent,
88 pub(super) unredacted_item: Option<UnredactedEventTimelineItem>,
93 pub(super) redaction_send_state: Option<EventSendState>,
95 pub(super) edit_send_state: Option<EventSendState>,
97 pub(super) reactions: ReactionsByKeyBySender,
99 pub(super) unedited_kind: Option<Box<MsgLikeKind>>,
101 pub(super) kind: EventTimelineItemKind,
103 pub(super) is_room_encrypted: bool,
107}
108
109#[derive(Clone, Debug)]
110pub(super) enum EventTimelineItemKind {
111 Local(LocalEventTimelineItem),
113 Remote(RemoteEventTimelineItem),
115}
116
117#[derive(Clone, Debug, Eq, Hash, PartialEq)]
119pub enum TimelineEventItemId {
120 TransactionId(OwnedTransactionId),
123 EventId(OwnedEventId),
125}
126
127pub(crate) enum TimelineItemHandle<'a> {
133 Remote(&'a EventId),
134 Local(&'a SendHandle),
135}
136
137#[derive(Clone, Debug)]
142pub struct EditRevision {
143 pub content: TimelineItemContent,
145 pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
147}
148
149#[derive(Clone, Debug)]
152pub(super) struct UnredactedEventTimelineItem {
153 content: TimelineItemContent,
155
156 reactions: ReactionsByKeyBySender,
158
159 pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
161
162 pub(crate) latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
164}
165
166impl EventTimelineItem {
167 #[allow(clippy::too_many_arguments)]
168 pub(super) fn new(
169 sender: OwnedUserId,
170 sender_profile: TimelineDetails<Profile>,
171 forwarder: Option<OwnedUserId>,
172 forwarder_profile: Option<TimelineDetails<Profile>>,
173 timestamp: MilliSecondsSinceUnixEpoch,
174 content: TimelineItemContent,
175 kind: EventTimelineItemKind,
176 is_room_encrypted: bool,
177 ) -> Self {
178 Self {
179 sender,
180 sender_profile,
181 forwarder,
182 forwarder_profile,
183 timestamp,
184 content,
185 unredacted_item: None,
186 redaction_send_state: None,
187 edit_send_state: None,
188 reactions: Default::default(),
189 unedited_kind: None,
190 kind,
191 is_room_encrypted,
192 }
193 }
194
195 pub fn reactions(&self) -> &ReactionsByKeyBySender {
197 &self.reactions
198 }
199
200 pub(crate) fn reactions_mut(&mut self) -> &mut ReactionsByKeyBySender {
202 &mut self.reactions
203 }
204
205 pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
207 Self { reactions, ..self.clone() }
208 }
209
210 pub fn is_local_echo(&self) -> bool {
217 matches!(self.kind, EventTimelineItemKind::Local(_))
218 }
219
220 pub fn is_remote_event(&self) -> bool {
228 matches!(self.kind, EventTimelineItemKind::Remote(_))
229 }
230
231 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
233 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
234 }
235
236 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
238 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
239 }
240
241 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
244 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
245 }
246
247 pub fn send_state(&self) -> Option<&EventSendState> {
249 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
250 }
251
252 pub fn redaction_send_state(&self) -> Option<&EventSendState> {
255 self.redaction_send_state.as_ref()
256 }
257
258 pub fn edit_send_state(&self) -> Option<&EventSendState> {
262 self.edit_send_state.as_ref()
263 }
264
265 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
267 match &self.kind {
268 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
269 EventTimelineItemKind::Remote(_) => None,
270 }
271 }
272
273 pub fn identifier(&self) -> TimelineEventItemId {
279 match &self.kind {
280 EventTimelineItemKind::Local(local) => local.identifier(),
281 EventTimelineItemKind::Remote(remote) => {
282 TimelineEventItemId::EventId(remote.event_id.clone())
283 }
284 }
285 }
286
287 pub fn transaction_id(&self) -> Option<&TransactionId> {
292 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
293 }
294
295 pub fn event_id(&self) -> Option<&EventId> {
304 match &self.kind {
305 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
306 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
307 }
308 }
309
310 pub fn sender(&self) -> &UserId {
312 &self.sender
313 }
314
315 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
317 &self.sender_profile
318 }
319
320 pub fn forwarder(&self) -> Option<&UserId> {
325 self.forwarder.as_deref()
326 }
327
328 pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
333 self.forwarder_profile.as_ref()
334 }
335
336 pub fn content(&self) -> &TimelineItemContent {
338 &self.content
339 }
340
341 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
343 &mut self.content
344 }
345
346 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
353 static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
354 LazyLock::new(Default::default);
355 match &self.kind {
356 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
357 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
358 }
359 }
360
361 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
367 self.timestamp
368 }
369
370 pub fn is_own(&self) -> bool {
372 match &self.kind {
373 EventTimelineItemKind::Local(_) => true,
374 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
375 }
376 }
377
378 pub fn is_editable(&self) -> bool {
380 if !self.is_own() {
384 return false;
386 }
387
388 match self.content() {
389 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
390 MsgLikeKind::Message(message) => match message.msgtype() {
391 MessageType::Text(_)
392 | MessageType::Emote(_)
393 | MessageType::Audio(_)
394 | MessageType::File(_)
395 | MessageType::Image(_)
396 | MessageType::Video(_) => true,
397 #[cfg(feature = "unstable-msc4274")]
398 MessageType::Gallery(_) => true,
399 _ => false,
400 },
401 MsgLikeKind::Poll(poll) => {
402 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
403 }
404 _ => false,
406 },
407 _ => {
408 false
410 }
411 }
412 }
413
414 pub fn is_highlighted(&self) -> bool {
416 match &self.kind {
417 EventTimelineItemKind::Local(_) => false,
418 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
419 }
420 }
421
422 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
424 match &self.kind {
425 EventTimelineItemKind::Local(_) => None,
426 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
427 }
428 }
429
430 pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
433 if !self.is_room_encrypted || self.is_local_echo() {
434 return TimelineEventShieldState::None;
435 }
436
437 if self.content().is_unable_to_decrypt() {
439 return TimelineEventShieldState::None;
440 }
441
442 if let Some(live_location) = self.content().as_live_location_state() {
453 return match live_location.latest_location() {
454 None => TimelineEventShieldState::None,
455 Some(beacon) => match beacon.encryption_info() {
456 Some(info) => {
457 if strict {
458 info.verification_state.to_shield_state_strict().into()
459 } else {
460 info.verification_state.to_shield_state_lax().into()
461 }
462 }
463 None => TimelineEventShieldState::Red {
464 code: TimelineEventShieldStateCode::SentInClear,
465 },
466 },
467 };
468 }
469
470 match self.encryption_info() {
471 Some(info) => {
472 if strict {
473 info.verification_state.to_shield_state_strict().into()
474 } else {
475 info.verification_state.to_shield_state_lax().into()
476 }
477 }
478 None => {
479 TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
480 }
481 }
482 }
483
484 pub fn can_be_replied_to(&self) -> bool {
486 if self.event_id().is_none() {
488 false
489 } else if self.content.is_message() {
490 true
491 } else if self.content().as_live_location_state().is_some() {
492 false
495 } else {
496 self.latest_json().is_some()
497 }
498 }
499
500 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
505 match &self.kind {
506 EventTimelineItemKind::Local(_) => None,
507 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
508 }
509 }
510
511 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
513 match &self.kind {
514 EventTimelineItemKind::Local(_) => None,
515 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
516 }
517 }
518
519 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
522 self.latest_edit_json().or_else(|| self.original_json())
523 }
524
525 pub fn origin(&self) -> Option<EventItemOrigin> {
529 match &self.kind {
530 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
531 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
532 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
533 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
534 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
535 RemoteEventOrigin::Unknown => None,
536 },
537 }
538 }
539
540 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
541 self.content = content;
542 }
543
544 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
546 Self { kind: kind.into(), ..self.clone() }
547 }
548
549 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
551 let mut new = self.clone();
552 new.content = new_content;
553 new
554 }
555
556 pub(super) fn with_content_and_latest_edit(
561 &self,
562 new_content: TimelineItemContent,
563 edit_json: Option<Raw<AnySyncTimelineEvent>>,
564 ) -> Self {
565 let mut new = self.clone();
566 new.content = new_content;
567 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
568 r.latest_edit_json = edit_json;
569 }
570 new
571 }
572
573 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
575 Self { sender_profile, ..self.clone() }
576 }
577
578 pub(super) fn with_encryption_info(
580 &self,
581 encryption_info: Option<Arc<EncryptionInfo>>,
582 ) -> Self {
583 let mut new = self.clone();
584 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
585 r.encryption_info = encryption_info;
586 }
587
588 new
589 }
590
591 pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
593 let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
594 content: self.content.clone(),
595 reactions: self.reactions.clone(),
596 original_json: self.original_json().cloned(),
597 latest_edit_json: self.latest_edit_json().cloned(),
598 });
599 let content = self.content.redact(rules);
600 let kind = match &self.kind {
601 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
602 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
603 };
604 Self {
605 sender: self.sender.clone(),
606 sender_profile: self.sender_profile.clone(),
607 forwarder: self.forwarder.clone(),
608 forwarder_profile: self.forwarder_profile.clone(),
609 timestamp: self.timestamp,
610 content,
611 unredacted_item,
612 redaction_send_state: None,
613 edit_send_state: None,
614 reactions: Default::default(),
615 unedited_kind: None,
616 kind,
617 is_room_encrypted: self.is_room_encrypted,
618 }
619 }
620
621 pub(super) fn unredact(&self) -> Self {
625 let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
626 let kind = match &self.kind {
627 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
628 EventTimelineItemKind::Remote(r) => {
629 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
630 original_json: unredacted_item.original_json.clone(),
631 latest_edit_json: unredacted_item.latest_edit_json.clone(),
632 ..r.clone()
633 })
634 }
635 };
636 Self {
637 sender: self.sender.clone(),
638 sender_profile: self.sender_profile.clone(),
639 forwarder: self.forwarder.clone(),
640 forwarder_profile: self.forwarder_profile.clone(),
641 timestamp: self.timestamp,
642 content: unredacted_item.content.clone(),
643 unredacted_item: None,
644 redaction_send_state: None,
645 edit_send_state: None,
646 reactions: unredacted_item.reactions.clone(),
647 unedited_kind: None,
648 kind,
649 is_room_encrypted: self.is_room_encrypted,
650 }
651 }
652
653 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
654 match &self.kind {
655 EventTimelineItemKind::Local(local) => {
656 if let Some(event_id) = local.event_id() {
657 TimelineItemHandle::Remote(event_id)
658 } else {
659 TimelineItemHandle::Local(
660 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
662 )
663 }
664 }
665 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
666 }
667 }
668
669 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
671 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
672 }
673
674 pub fn contains_only_emojis(&self) -> bool {
701 let body = match self.content() {
702 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
703 MsgLikeKind::Message(message) => match &message.msgtype {
704 MessageType::Text(text) => Some(text.body.as_str()),
705 MessageType::Audio(audio) => audio.caption(),
706 MessageType::File(file) => file.caption(),
707 MessageType::Image(image) => image.caption(),
708 MessageType::Video(video) => video.caption(),
709 _ => None,
710 },
711 MsgLikeKind::Sticker(_)
712 | MsgLikeKind::Poll(_)
713 | MsgLikeKind::Redacted
714 | MsgLikeKind::UnableToDecrypt(_)
715 | MsgLikeKind::Other(_)
716 | MsgLikeKind::LiveLocation(_) => None,
717 },
718 TimelineItemContent::MembershipChange(_)
719 | TimelineItemContent::ProfileChange(_)
720 | TimelineItemContent::OtherState(_)
721 | TimelineItemContent::FailedToParseMessageLike { .. }
722 | TimelineItemContent::FailedToParseState { .. }
723 | TimelineItemContent::CallInvite
724 | TimelineItemContent::RtcNotification { .. } => None,
725 };
726
727 if let Some(body) = body {
728 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
730
731 if graphemes.len() > 5 {
736 return false;
737 }
738
739 graphemes.iter().all(|g| emojis::get(g).is_some())
740 } else {
741 false
742 }
743 }
744}
745
746impl From<LocalEventTimelineItem> for EventTimelineItemKind {
747 fn from(value: LocalEventTimelineItem) -> Self {
748 EventTimelineItemKind::Local(value)
749 }
750}
751
752impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
753 fn from(value: RemoteEventTimelineItem) -> Self {
754 EventTimelineItemKind::Remote(value)
755 }
756}
757
758#[derive(Clone, Debug, Default, PartialEq, Eq)]
760pub struct Profile {
761 pub display_name: Option<String>,
763
764 pub display_name_ambiguous: bool,
770
771 pub avatar_url: Option<OwnedMxcUri>,
773
774 #[cfg(feature = "unstable-msc4426")]
776 pub status: Option<StatusProfileField>,
777
778 #[cfg(feature = "unstable-msc4426")]
780 pub call: Option<CallProfileField>,
781}
782
783impl Profile {
784 pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
785 match room.get_member_no_sync(user_id).await {
786 Ok(Some(member)) => Some(Profile {
787 display_name: member.display_name().map(ToOwned::to_owned),
788 display_name_ambiguous: member.name_ambiguous(),
789 avatar_url: member.avatar_url().map(ToOwned::to_owned),
790 #[cfg(feature = "unstable-msc4426")]
791 status: member.status().cloned(),
792 #[cfg(feature = "unstable-msc4426")]
793 call: member.call().cloned(),
794 }),
795 Ok(None) if room.are_members_synced() => Some(Profile::default()),
796 Ok(None) => None,
797 Err(e) => {
798 error!(%user_id, "Failed to fetch room member information: {e}");
799 None
800 }
801 }
802 }
803}
804
805#[derive(Clone, Debug)]
809pub enum TimelineDetails<T> {
810 Unavailable,
813
814 Pending,
816
817 Ready(T),
819
820 Error(Arc<Error>),
822}
823
824impl<T> TimelineDetails<T> {
825 pub fn from_initial_value(value: Option<T>) -> Self {
831 match value {
832 Some(v) => Self::Ready(v),
833 None => Self::Unavailable,
834 }
835 }
836
837 pub fn is_unavailable(&self) -> bool {
838 matches!(self, Self::Unavailable)
839 }
840
841 pub fn is_ready(&self) -> bool {
842 matches!(self, Self::Ready(_))
843 }
844}
845
846#[derive(Clone, Copy, Debug)]
848#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
849pub enum EventItemOrigin {
850 Local,
852 Sync,
854 Pagination,
856 Cache,
858}
859
860#[derive(Clone, Debug)]
862pub struct ReactionInfo {
863 pub timestamp: MilliSecondsSinceUnixEpoch,
864 pub send_state: Option<EventSendState>,
867}
868
869#[derive(Debug, Clone, Default)]
874pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
875
876impl Deref for ReactionsByKeyBySender {
877 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
878
879 fn deref(&self) -> &Self::Target {
880 &self.0
881 }
882}
883
884impl DerefMut for ReactionsByKeyBySender {
885 fn deref_mut(&mut self) -> &mut Self::Target {
886 &mut self.0
887 }
888}
889
890impl ReactionsByKeyBySender {
891 pub(crate) fn remove_reaction(
897 &mut self,
898 sender: &UserId,
899 annotation: &str,
900 ) -> Option<ReactionInfo> {
901 if let Some(by_user) = self.0.get_mut(annotation)
902 && let Some(info) = by_user.swap_remove(sender)
903 {
904 if by_user.is_empty() {
906 self.0.swap_remove(annotation);
907 }
908 return Some(info);
909 }
910 None
911 }
912}
913
914#[derive(Clone, Copy, Debug, Eq, PartialEq)]
916pub enum TimelineEventShieldState {
917 Red {
920 code: TimelineEventShieldStateCode,
922 },
923 Grey {
926 code: TimelineEventShieldStateCode,
928 },
929 None,
931}
932
933impl From<ShieldState> for TimelineEventShieldState {
934 fn from(value: ShieldState) -> Self {
935 match value {
936 ShieldState::Red { code, message: _ } => {
937 TimelineEventShieldState::Red { code: code.into() }
938 }
939 ShieldState::Grey { code, message: _ } => {
940 TimelineEventShieldState::Grey { code: code.into() }
941 }
942 ShieldState::None => TimelineEventShieldState::None,
943 }
944 }
945}
946
947#[derive(Clone, Copy, Debug, Eq, PartialEq)]
949#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
950pub enum TimelineEventShieldStateCode {
951 AuthenticityNotGuaranteed,
953 UnknownDevice,
955 UnsignedDevice,
957 UnverifiedIdentity,
959 VerificationViolation,
961 MismatchedSender,
964 SentInClear,
966}
967
968impl From<ShieldStateCode> for TimelineEventShieldStateCode {
969 fn from(value: ShieldStateCode) -> Self {
970 use TimelineEventShieldStateCode::*;
971 match value {
972 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
973 ShieldStateCode::UnknownDevice => UnknownDevice,
974 ShieldStateCode::UnsignedDevice => UnsignedDevice,
975 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
976 ShieldStateCode::VerificationViolation => VerificationViolation,
977 ShieldStateCode::MismatchedSender => MismatchedSender,
978 }
979 }
980}
981
982#[cfg(test)]
983mod tests {
984 use std::time::Duration;
985
986 use ruma::{
987 MilliSecondsSinceUnixEpoch,
988 events::{
989 AnySyncTimelineEvent,
990 beacon_info::BeaconInfoEventContent,
991 room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
992 },
993 owned_event_id, owned_user_id,
994 serde::Raw,
995 uint,
996 };
997 use serde_json::json;
998
999 use super::{
1000 EventSendState, EventTimelineItem, EventTimelineItemKind, LiveLocationState,
1001 LocalEventTimelineItem, Message, MsgLikeContent, MsgLikeKind, RemoteEventOrigin,
1002 RemoteEventTimelineItem, TimelineDetails, TimelineItemContent,
1003 };
1004
1005 fn message_content() -> TimelineItemContent {
1006 TimelineItemContent::MsgLike(MsgLikeContent {
1007 kind: MsgLikeKind::Message(Message {
1008 msgtype: MessageType::Text(TextMessageEventContent::plain("hello")),
1009 edited: false,
1010 mentions: None,
1011 }),
1012 thread_root: None,
1013 in_reply_to: None,
1014 thread_summary: None,
1015 })
1016 }
1017
1018 fn live_location_content() -> TimelineItemContent {
1019 TimelineItemContent::MsgLike(MsgLikeContent {
1020 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(BeaconInfoEventContent::new(
1021 None,
1022 Duration::from_secs(300),
1023 true,
1024 Some(MilliSecondsSinceUnixEpoch(uint!(1))),
1025 ))),
1026 thread_root: None,
1027 in_reply_to: None,
1028 thread_summary: None,
1029 })
1030 }
1031
1032 fn remote_item(
1033 content: TimelineItemContent,
1034 original_json: Option<Raw<AnySyncTimelineEvent>>,
1035 ) -> EventTimelineItem {
1036 EventTimelineItem::new(
1037 owned_user_id!("@alice:example.org"),
1038 TimelineDetails::Unavailable,
1039 None,
1040 None,
1041 MilliSecondsSinceUnixEpoch(uint!(1)),
1042 content,
1043 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
1044 event_id: owned_event_id!("$event"),
1045 transaction_id: None,
1046 read_receipts: Default::default(),
1047 is_own: false,
1048 is_highlighted: false,
1049 encryption_info: None,
1050 original_json,
1051 latest_edit_json: None,
1052 origin: RemoteEventOrigin::Sync,
1053 }),
1054 false,
1055 )
1056 }
1057
1058 fn local_unsent_item(content: TimelineItemContent) -> EventTimelineItem {
1059 EventTimelineItem::new(
1060 owned_user_id!("@alice:example.org"),
1061 TimelineDetails::Unavailable,
1062 None,
1063 None,
1064 MilliSecondsSinceUnixEpoch(uint!(1)),
1065 content,
1066 EventTimelineItemKind::Local(LocalEventTimelineItem {
1067 send_state: EventSendState::NotSentYet { progress: None },
1068 transaction_id: "t0".into(),
1069 send_handle: None,
1070 }),
1071 false,
1072 )
1073 }
1074
1075 fn sample_raw_event() -> Raw<AnySyncTimelineEvent> {
1076 Raw::from_json_string(
1077 json!({
1078 "content": RoomMessageEventContent::text_plain("hi"),
1079 "type": "m.room.message",
1080 "event_id": "$event",
1081 "room_id": "!room:example.org",
1082 "origin_server_ts": 1,
1083 "sender": "@alice:example.org",
1084 })
1085 .to_string(),
1086 )
1087 .unwrap()
1088 }
1089
1090 #[test]
1091 fn cannot_reply_to_local_unsent_events() {
1092 let item = local_unsent_item(message_content());
1093 assert!(!item.can_be_replied_to());
1094 }
1095
1096 #[test]
1097 fn can_reply_to_messages() {
1098 let item = remote_item(message_content(), None);
1099 assert!(item.can_be_replied_to());
1100 }
1101
1102 #[test]
1103 fn cannot_reply_to_live_location_events() {
1104 let item = remote_item(live_location_content(), Some(sample_raw_event()));
1105 assert!(!item.can_be_replied_to());
1106 }
1107
1108 #[test]
1109 fn cannot_reply_to_non_messages_with_no_json() {
1110 let item = remote_item(TimelineItemContent::CallInvite, None);
1111 assert!(!item.can_be_replied_to());
1112 }
1113
1114 #[test]
1115 fn can_reply_to_non_messages_with_json() {
1116 let item = remote_item(TimelineItemContent::CallInvite, Some(sample_raw_event()));
1117 assert!(item.can_be_replied_to());
1118 }
1119}