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) redaction_reason: Option<String>,
99 pub(super) edit_send_state: Option<EventSendState>,
101 pub(super) reactions: ReactionsByKeyBySender,
103 pub(super) unedited_kind: Option<Box<MsgLikeKind>>,
105 pub(super) kind: EventTimelineItemKind,
107 pub(super) is_room_encrypted: bool,
111}
112
113#[derive(Clone, Debug)]
114pub(super) enum EventTimelineItemKind {
115 Local(LocalEventTimelineItem),
117 Remote(RemoteEventTimelineItem),
119}
120
121#[derive(Clone, Debug, Eq, Hash, PartialEq)]
123pub enum TimelineEventItemId {
124 TransactionId(OwnedTransactionId),
127 EventId(OwnedEventId),
129}
130
131pub(crate) enum TimelineItemHandle<'a> {
137 Remote(&'a EventId),
138 Local(&'a SendHandle),
139}
140
141#[derive(Clone, Debug)]
146pub struct EditRevision {
147 pub content: TimelineItemContent,
149 pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
151}
152
153#[derive(Clone, Debug)]
156pub(super) struct UnredactedEventTimelineItem {
157 content: TimelineItemContent,
159
160 reactions: ReactionsByKeyBySender,
162
163 pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
165
166 pub(crate) latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
168}
169
170impl EventTimelineItem {
171 #[allow(clippy::too_many_arguments)]
172 pub(super) fn new(
173 sender: OwnedUserId,
174 sender_profile: TimelineDetails<Profile>,
175 forwarder: Option<OwnedUserId>,
176 forwarder_profile: Option<TimelineDetails<Profile>>,
177 timestamp: MilliSecondsSinceUnixEpoch,
178 content: TimelineItemContent,
179 kind: EventTimelineItemKind,
180 is_room_encrypted: bool,
181 ) -> Self {
182 Self {
183 sender,
184 sender_profile,
185 forwarder,
186 forwarder_profile,
187 timestamp,
188 content,
189 unredacted_item: None,
190 redaction_send_state: None,
191 redaction_reason: None,
192 edit_send_state: None,
193 reactions: Default::default(),
194 unedited_kind: None,
195 kind,
196 is_room_encrypted,
197 }
198 }
199
200 pub fn reactions(&self) -> &ReactionsByKeyBySender {
202 &self.reactions
203 }
204
205 pub(crate) fn reactions_mut(&mut self) -> &mut ReactionsByKeyBySender {
207 &mut self.reactions
208 }
209
210 pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
212 Self { reactions, ..self.clone() }
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 redaction_send_state(&self) -> Option<&EventSendState> {
260 self.redaction_send_state.as_ref()
261 }
262
263 pub fn redaction_reason(&self) -> Option<&str> {
267 self.redaction_reason.as_deref()
268 }
269
270 pub fn edit_send_state(&self) -> Option<&EventSendState> {
274 self.edit_send_state.as_ref()
275 }
276
277 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
279 match &self.kind {
280 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
281 EventTimelineItemKind::Remote(_) => None,
282 }
283 }
284
285 pub fn identifier(&self) -> TimelineEventItemId {
291 match &self.kind {
292 EventTimelineItemKind::Local(local) => local.identifier(),
293 EventTimelineItemKind::Remote(remote) => {
294 TimelineEventItemId::EventId(remote.event_id.clone())
295 }
296 }
297 }
298
299 pub fn transaction_id(&self) -> Option<&TransactionId> {
304 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
305 }
306
307 pub fn event_id(&self) -> Option<&EventId> {
316 match &self.kind {
317 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
318 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
319 }
320 }
321
322 pub fn sender(&self) -> &UserId {
324 &self.sender
325 }
326
327 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
329 &self.sender_profile
330 }
331
332 pub fn forwarder(&self) -> Option<&UserId> {
337 self.forwarder.as_deref()
338 }
339
340 pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
345 self.forwarder_profile.as_ref()
346 }
347
348 pub fn content(&self) -> &TimelineItemContent {
350 &self.content
351 }
352
353 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
355 &mut self.content
356 }
357
358 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
365 static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
366 LazyLock::new(Default::default);
367 match &self.kind {
368 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
369 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
370 }
371 }
372
373 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
379 self.timestamp
380 }
381
382 pub fn is_own(&self) -> bool {
384 match &self.kind {
385 EventTimelineItemKind::Local(_) => true,
386 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
387 }
388 }
389
390 pub fn is_editable(&self) -> bool {
392 if !self.is_own() {
396 return false;
398 }
399
400 match self.content() {
401 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
402 MsgLikeKind::Message(message) => match message.msgtype() {
403 MessageType::Text(_)
404 | MessageType::Emote(_)
405 | MessageType::Audio(_)
406 | MessageType::File(_)
407 | MessageType::Image(_)
408 | MessageType::Video(_) => true,
409 #[cfg(feature = "unstable-msc4274")]
410 MessageType::Gallery(_) => true,
411 _ => false,
412 },
413 MsgLikeKind::Poll(poll) => {
414 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
415 }
416 _ => false,
418 },
419 _ => {
420 false
422 }
423 }
424 }
425
426 pub fn is_highlighted(&self) -> bool {
428 match &self.kind {
429 EventTimelineItemKind::Local(_) => false,
430 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
431 }
432 }
433
434 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
436 match &self.kind {
437 EventTimelineItemKind::Local(_) => None,
438 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
439 }
440 }
441
442 pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
445 if !self.is_room_encrypted || self.is_local_echo() {
446 return TimelineEventShieldState::None;
447 }
448
449 if self.content().is_unable_to_decrypt() {
451 return TimelineEventShieldState::None;
452 }
453
454 if let Some(live_location) = self.content().as_live_location_state() {
465 return match live_location.latest_location() {
466 None => TimelineEventShieldState::None,
467 Some(beacon) => match beacon.encryption_info() {
468 Some(info) => {
469 if strict {
470 info.verification_state.to_shield_state_strict().into()
471 } else {
472 info.verification_state.to_shield_state_lax().into()
473 }
474 }
475 None => TimelineEventShieldState::Red {
476 code: TimelineEventShieldStateCode::SentInClear,
477 },
478 },
479 };
480 }
481
482 match self.encryption_info() {
483 Some(info) => {
484 if strict {
485 info.verification_state.to_shield_state_strict().into()
486 } else {
487 info.verification_state.to_shield_state_lax().into()
488 }
489 }
490 None => {
491 TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
492 }
493 }
494 }
495
496 pub fn can_be_replied_to(&self) -> bool {
498 if self.event_id().is_none() {
500 false
501 } else if self.content.is_message() {
502 true
503 } else if self.content().as_live_location_state().is_some() {
504 false
507 } else {
508 self.latest_json().is_some()
509 }
510 }
511
512 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
517 match &self.kind {
518 EventTimelineItemKind::Local(_) => None,
519 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
520 }
521 }
522
523 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
525 match &self.kind {
526 EventTimelineItemKind::Local(_) => None,
527 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
528 }
529 }
530
531 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
534 self.latest_edit_json().or_else(|| self.original_json())
535 }
536
537 pub fn origin(&self) -> Option<EventItemOrigin> {
541 match &self.kind {
542 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
543 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
544 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
545 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
546 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
547 RemoteEventOrigin::Unknown => None,
548 },
549 }
550 }
551
552 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
553 self.content = content;
554 }
555
556 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
558 Self { kind: kind.into(), ..self.clone() }
559 }
560
561 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
563 let mut new = self.clone();
564 new.content = new_content;
565 new
566 }
567
568 pub(super) fn with_content_and_latest_edit(
573 &self,
574 new_content: TimelineItemContent,
575 edit_json: Option<Raw<AnySyncTimelineEvent>>,
576 ) -> Self {
577 let mut new = self.clone();
578 new.content = new_content;
579 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
580 r.latest_edit_json = edit_json;
581 }
582 new
583 }
584
585 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
587 Self { sender_profile, ..self.clone() }
588 }
589
590 pub(super) fn with_encryption_info(
592 &self,
593 encryption_info: Option<Arc<EncryptionInfo>>,
594 ) -> Self {
595 let mut new = self.clone();
596 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
597 r.encryption_info = encryption_info;
598 }
599
600 new
601 }
602
603 pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
605 let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
606 content: self.content.clone(),
607 reactions: self.reactions.clone(),
608 original_json: self.original_json().cloned(),
609 latest_edit_json: self.latest_edit_json().cloned(),
610 });
611 let content = self.content.redact(rules);
612 let kind = match &self.kind {
613 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
614 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
615 };
616 Self {
617 sender: self.sender.clone(),
618 sender_profile: self.sender_profile.clone(),
619 forwarder: self.forwarder.clone(),
620 forwarder_profile: self.forwarder_profile.clone(),
621 timestamp: self.timestamp,
622 content,
623 unredacted_item,
624 redaction_send_state: None,
625 redaction_reason: None,
626 edit_send_state: None,
627 reactions: Default::default(),
628 unedited_kind: None,
629 kind,
630 is_room_encrypted: self.is_room_encrypted,
631 }
632 }
633
634 pub(super) fn unredact(&self) -> Self {
638 let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
639 let kind = match &self.kind {
640 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
641 EventTimelineItemKind::Remote(r) => {
642 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
643 original_json: unredacted_item.original_json.clone(),
644 latest_edit_json: unredacted_item.latest_edit_json.clone(),
645 ..r.clone()
646 })
647 }
648 };
649 Self {
650 sender: self.sender.clone(),
651 sender_profile: self.sender_profile.clone(),
652 forwarder: self.forwarder.clone(),
653 forwarder_profile: self.forwarder_profile.clone(),
654 timestamp: self.timestamp,
655 content: unredacted_item.content.clone(),
656 unredacted_item: None,
657 redaction_send_state: None,
658 redaction_reason: None,
659 edit_send_state: None,
660 reactions: unredacted_item.reactions.clone(),
661 unedited_kind: None,
662 kind,
663 is_room_encrypted: self.is_room_encrypted,
664 }
665 }
666
667 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
668 match &self.kind {
669 EventTimelineItemKind::Local(local) => {
670 if let Some(event_id) = local.event_id() {
671 TimelineItemHandle::Remote(event_id)
672 } else {
673 TimelineItemHandle::Local(
674 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
676 )
677 }
678 }
679 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
680 }
681 }
682
683 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
685 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
686 }
687
688 pub fn contains_only_emojis(&self) -> bool {
715 let body = match self.content() {
716 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
717 MsgLikeKind::Message(message) => match &message.msgtype {
718 MessageType::Text(text) => Some(text.body.as_str()),
719 MessageType::Audio(audio) => audio.caption(),
720 MessageType::File(file) => file.caption(),
721 MessageType::Image(image) => image.caption(),
722 MessageType::Video(video) => video.caption(),
723 _ => None,
724 },
725 MsgLikeKind::Sticker(_)
726 | MsgLikeKind::Poll(_)
727 | MsgLikeKind::Redacted
728 | MsgLikeKind::UnableToDecrypt(_)
729 | MsgLikeKind::Other(_)
730 | MsgLikeKind::LiveLocation(_) => None,
731 },
732 TimelineItemContent::MembershipChange(_)
733 | TimelineItemContent::ProfileChange(_)
734 | TimelineItemContent::OtherState(_)
735 | TimelineItemContent::FailedToParseMessageLike { .. }
736 | TimelineItemContent::FailedToParseState { .. }
737 | TimelineItemContent::CallInvite
738 | TimelineItemContent::RtcNotification { .. } => None,
739 };
740
741 if let Some(body) = body {
742 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
744
745 if graphemes.len() > 5 {
750 return false;
751 }
752
753 graphemes.iter().all(|g| emojis::get(g).is_some())
754 } else {
755 false
756 }
757 }
758}
759
760impl From<LocalEventTimelineItem> for EventTimelineItemKind {
761 fn from(value: LocalEventTimelineItem) -> Self {
762 EventTimelineItemKind::Local(value)
763 }
764}
765
766impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
767 fn from(value: RemoteEventTimelineItem) -> Self {
768 EventTimelineItemKind::Remote(value)
769 }
770}
771
772#[derive(Clone, Debug, Default, PartialEq, Eq)]
774pub struct Profile {
775 pub display_name: Option<String>,
777
778 pub display_name_ambiguous: bool,
784
785 pub avatar_url: Option<OwnedMxcUri>,
787
788 #[cfg(feature = "unstable-msc4426")]
790 pub status: Option<StatusProfileField>,
791
792 #[cfg(feature = "unstable-msc4426")]
794 pub call: Option<CallProfileField>,
795}
796
797impl Profile {
798 pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
799 match room.get_member_no_sync(user_id).await {
800 Ok(Some(member)) => Some(Profile {
801 display_name: member.display_name().map(ToOwned::to_owned),
802 display_name_ambiguous: member.name_ambiguous(),
803 avatar_url: member.avatar_url().map(ToOwned::to_owned),
804 #[cfg(feature = "unstable-msc4426")]
805 status: member.status().cloned(),
806 #[cfg(feature = "unstable-msc4426")]
807 call: member.call().cloned(),
808 }),
809 Ok(None) if room.are_members_synced() => Some(Profile::default()),
810 Ok(None) => None,
811 Err(e) => {
812 error!(%user_id, "Failed to fetch room member information: {e}");
813 None
814 }
815 }
816 }
817}
818
819#[derive(Clone, Debug)]
823pub enum TimelineDetails<T> {
824 Unavailable,
827
828 Pending,
830
831 Ready(T),
833
834 Error(Arc<Error>),
836}
837
838impl<T> TimelineDetails<T> {
839 pub fn from_initial_value(value: Option<T>) -> Self {
845 match value {
846 Some(v) => Self::Ready(v),
847 None => Self::Unavailable,
848 }
849 }
850
851 pub fn is_unavailable(&self) -> bool {
852 matches!(self, Self::Unavailable)
853 }
854
855 pub fn is_ready(&self) -> bool {
856 matches!(self, Self::Ready(_))
857 }
858}
859
860#[derive(Clone, Copy, Debug)]
862#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
863pub enum EventItemOrigin {
864 Local,
866 Sync,
868 Pagination,
870 Cache,
872}
873
874#[derive(Clone, Debug)]
876pub struct ReactionInfo {
877 pub timestamp: MilliSecondsSinceUnixEpoch,
878 pub send_state: Option<EventSendState>,
881}
882
883#[derive(Debug, Clone, Default)]
888pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
889
890impl Deref for ReactionsByKeyBySender {
891 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
892
893 fn deref(&self) -> &Self::Target {
894 &self.0
895 }
896}
897
898impl DerefMut for ReactionsByKeyBySender {
899 fn deref_mut(&mut self) -> &mut Self::Target {
900 &mut self.0
901 }
902}
903
904impl ReactionsByKeyBySender {
905 pub(crate) fn remove_reaction(
911 &mut self,
912 sender: &UserId,
913 annotation: &str,
914 ) -> Option<ReactionInfo> {
915 if let Some(by_user) = self.0.get_mut(annotation)
916 && let Some(info) = by_user.swap_remove(sender)
917 {
918 if by_user.is_empty() {
920 self.0.swap_remove(annotation);
921 }
922 return Some(info);
923 }
924 None
925 }
926}
927
928#[derive(Clone, Copy, Debug, Eq, PartialEq)]
930pub enum TimelineEventShieldState {
931 Red {
934 code: TimelineEventShieldStateCode,
936 },
937 Grey {
940 code: TimelineEventShieldStateCode,
942 },
943 None,
945}
946
947impl From<ShieldState> for TimelineEventShieldState {
948 fn from(value: ShieldState) -> Self {
949 match value {
950 ShieldState::Red { code, message: _ } => {
951 TimelineEventShieldState::Red { code: code.into() }
952 }
953 ShieldState::Grey { code, message: _ } => {
954 TimelineEventShieldState::Grey { code: code.into() }
955 }
956 ShieldState::None => TimelineEventShieldState::None,
957 }
958 }
959}
960
961#[derive(Clone, Copy, Debug, Eq, PartialEq)]
963#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
964pub enum TimelineEventShieldStateCode {
965 AuthenticityNotGuaranteed,
967 UnknownDevice,
969 UnsignedDevice,
971 UnverifiedIdentity,
973 VerificationViolation,
975 MismatchedSender,
978 SentInClear,
980}
981
982impl From<ShieldStateCode> for TimelineEventShieldStateCode {
983 fn from(value: ShieldStateCode) -> Self {
984 use TimelineEventShieldStateCode::*;
985 match value {
986 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
987 ShieldStateCode::UnknownDevice => UnknownDevice,
988 ShieldStateCode::UnsignedDevice => UnsignedDevice,
989 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
990 ShieldStateCode::VerificationViolation => VerificationViolation,
991 ShieldStateCode::MismatchedSender => MismatchedSender,
992 }
993 }
994}
995
996#[cfg(test)]
997mod tests {
998 use std::time::Duration;
999
1000 use ruma::{
1001 MilliSecondsSinceUnixEpoch,
1002 events::{
1003 AnySyncTimelineEvent,
1004 beacon_info::BeaconInfoEventContent,
1005 room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
1006 },
1007 owned_event_id, owned_user_id,
1008 serde::Raw,
1009 uint,
1010 };
1011 use serde_json::json;
1012
1013 use super::{
1014 EventSendState, EventTimelineItem, EventTimelineItemKind, LiveLocationState,
1015 LocalEventTimelineItem, Message, MsgLikeContent, MsgLikeKind, RemoteEventOrigin,
1016 RemoteEventTimelineItem, TimelineDetails, TimelineItemContent,
1017 };
1018
1019 fn message_content() -> TimelineItemContent {
1020 TimelineItemContent::MsgLike(MsgLikeContent {
1021 kind: MsgLikeKind::Message(Message {
1022 msgtype: MessageType::Text(TextMessageEventContent::plain("hello")),
1023 edited: false,
1024 mentions: None,
1025 }),
1026 thread_root: None,
1027 in_reply_to: None,
1028 thread_summary: None,
1029 })
1030 }
1031
1032 fn live_location_content() -> TimelineItemContent {
1033 TimelineItemContent::MsgLike(MsgLikeContent {
1034 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(BeaconInfoEventContent::new(
1035 None,
1036 Duration::from_secs(300),
1037 true,
1038 Some(MilliSecondsSinceUnixEpoch(uint!(1))),
1039 ))),
1040 thread_root: None,
1041 in_reply_to: None,
1042 thread_summary: None,
1043 })
1044 }
1045
1046 fn remote_item(
1047 content: TimelineItemContent,
1048 original_json: Option<Raw<AnySyncTimelineEvent>>,
1049 ) -> EventTimelineItem {
1050 EventTimelineItem::new(
1051 owned_user_id!("@alice:example.org"),
1052 TimelineDetails::Unavailable,
1053 None,
1054 None,
1055 MilliSecondsSinceUnixEpoch(uint!(1)),
1056 content,
1057 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
1058 event_id: owned_event_id!("$event"),
1059 transaction_id: None,
1060 read_receipts: Default::default(),
1061 is_own: false,
1062 is_highlighted: false,
1063 encryption_info: None,
1064 original_json,
1065 latest_edit_json: None,
1066 origin: RemoteEventOrigin::Sync,
1067 }),
1068 false,
1069 )
1070 }
1071
1072 fn local_unsent_item(content: TimelineItemContent) -> EventTimelineItem {
1073 EventTimelineItem::new(
1074 owned_user_id!("@alice:example.org"),
1075 TimelineDetails::Unavailable,
1076 None,
1077 None,
1078 MilliSecondsSinceUnixEpoch(uint!(1)),
1079 content,
1080 EventTimelineItemKind::Local(LocalEventTimelineItem {
1081 send_state: EventSendState::NotSentYet { progress: None },
1082 transaction_id: "t0".into(),
1083 send_handle: None,
1084 }),
1085 false,
1086 )
1087 }
1088
1089 fn sample_raw_event() -> Raw<AnySyncTimelineEvent> {
1090 Raw::from_json_string(
1091 json!({
1092 "content": RoomMessageEventContent::text_plain("hi"),
1093 "type": "m.room.message",
1094 "event_id": "$event",
1095 "room_id": "!room:example.org",
1096 "origin_server_ts": 1,
1097 "sender": "@alice:example.org",
1098 })
1099 .to_string(),
1100 )
1101 .unwrap()
1102 }
1103
1104 #[test]
1105 fn cannot_reply_to_local_unsent_events() {
1106 let item = local_unsent_item(message_content());
1107 assert!(!item.can_be_replied_to());
1108 }
1109
1110 #[test]
1111 fn can_reply_to_messages() {
1112 let item = remote_item(message_content(), None);
1113 assert!(item.can_be_replied_to());
1114 }
1115
1116 #[test]
1117 fn cannot_reply_to_live_location_events() {
1118 let item = remote_item(live_location_content(), Some(sample_raw_event()));
1119 assert!(!item.can_be_replied_to());
1120 }
1121
1122 #[test]
1123 fn cannot_reply_to_non_messages_with_no_json() {
1124 let item = remote_item(TimelineItemContent::CallInvite, None);
1125 assert!(!item.can_be_replied_to());
1126 }
1127
1128 #[test]
1129 fn can_reply_to_non_messages_with_json() {
1130 let item = remote_item(TimelineItemContent::CallInvite, Some(sample_raw_event()));
1131 assert!(item.can_be_replied_to());
1132 }
1133}