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, SendReactionHandle},
26};
27use matrix_sdk_base::deserialized_responses::ShieldStateCode;
28use ruma::{
29 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
30 OwnedUserId, TransactionId, UserId,
31 events::{AnySyncTimelineEvent, receipt::Receipt, room::message::MessageType},
32 room_version_rules::RedactionRules,
33 serde::Raw,
34};
35use tracing::error;
36use unicode_segmentation::UnicodeSegmentation;
37
38mod content;
39mod local;
40mod remote;
41
42pub use self::{
43 content::{
44 AnyOtherStateEventContentChange, BeaconInfo, EmbeddedEvent, EncryptedMessage,
45 InReplyToDetails, LiveLocationState, MemberProfileChange, MembershipChange, Message,
46 MsgLikeContent, MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState,
47 RoomMembershipChange, RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
48 },
49 local::{EventSendState, MediaUploadProgress},
50};
51pub(super) use self::{
52 content::{
53 beacon_info_matches, extract_bundled_edit_event_json, extract_poll_edit_content,
54 extract_room_msg_edit_content,
55 },
56 local::LocalEventTimelineItem,
57 remote::{RemoteEventOrigin, RemoteEventTimelineItem},
58};
59
60#[derive(Clone, Debug)]
66pub struct EventTimelineItem {
67 pub(super) sender: OwnedUserId,
69 pub(super) sender_profile: TimelineDetails<Profile>,
71 pub(super) forwarder: Option<OwnedUserId>,
76 pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
81 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
83 pub(super) content: TimelineItemContent,
86 pub(super) unredacted_item: Option<UnredactedEventTimelineItem>,
91 pub(super) kind: EventTimelineItemKind,
93 pub(super) is_room_encrypted: bool,
97}
98
99#[derive(Clone, Debug)]
100pub(super) enum EventTimelineItemKind {
101 Local(LocalEventTimelineItem),
103 Remote(RemoteEventTimelineItem),
105}
106
107#[derive(Clone, Debug, Eq, Hash, PartialEq)]
109pub enum TimelineEventItemId {
110 TransactionId(OwnedTransactionId),
113 EventId(OwnedEventId),
115}
116
117pub(crate) enum TimelineItemHandle<'a> {
123 Remote(&'a EventId),
124 Local(&'a SendHandle),
125}
126
127#[derive(Clone, Debug)]
130pub(super) struct UnredactedEventTimelineItem {
131 content: TimelineItemContent,
133
134 pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
136
137 pub(crate) latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
139}
140
141impl EventTimelineItem {
142 #[allow(clippy::too_many_arguments)]
143 pub(super) fn new(
144 sender: OwnedUserId,
145 sender_profile: TimelineDetails<Profile>,
146 forwarder: Option<OwnedUserId>,
147 forwarder_profile: Option<TimelineDetails<Profile>>,
148 timestamp: MilliSecondsSinceUnixEpoch,
149 content: TimelineItemContent,
150 kind: EventTimelineItemKind,
151 is_room_encrypted: bool,
152 ) -> Self {
153 Self {
154 sender,
155 sender_profile,
156 forwarder,
157 forwarder_profile,
158 timestamp,
159 content,
160 unredacted_item: None,
161 kind,
162 is_room_encrypted,
163 }
164 }
165
166 pub fn is_local_echo(&self) -> bool {
173 matches!(self.kind, EventTimelineItemKind::Local(_))
174 }
175
176 pub fn is_remote_event(&self) -> bool {
184 matches!(self.kind, EventTimelineItemKind::Remote(_))
185 }
186
187 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
189 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
190 }
191
192 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
194 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
195 }
196
197 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
200 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
201 }
202
203 pub fn send_state(&self) -> Option<&EventSendState> {
205 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
206 }
207
208 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
210 match &self.kind {
211 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
212 EventTimelineItemKind::Remote(_) => None,
213 }
214 }
215
216 pub fn identifier(&self) -> TimelineEventItemId {
222 match &self.kind {
223 EventTimelineItemKind::Local(local) => local.identifier(),
224 EventTimelineItemKind::Remote(remote) => {
225 TimelineEventItemId::EventId(remote.event_id.clone())
226 }
227 }
228 }
229
230 pub fn transaction_id(&self) -> Option<&TransactionId> {
235 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
236 }
237
238 pub fn event_id(&self) -> Option<&EventId> {
247 match &self.kind {
248 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
249 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
250 }
251 }
252
253 pub fn sender(&self) -> &UserId {
255 &self.sender
256 }
257
258 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
260 &self.sender_profile
261 }
262
263 pub fn forwarder(&self) -> Option<&UserId> {
268 self.forwarder.as_deref()
269 }
270
271 pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
276 self.forwarder_profile.as_ref()
277 }
278
279 pub fn content(&self) -> &TimelineItemContent {
281 &self.content
282 }
283
284 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
286 &mut self.content
287 }
288
289 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
296 static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
297 LazyLock::new(Default::default);
298 match &self.kind {
299 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
300 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
301 }
302 }
303
304 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
310 self.timestamp
311 }
312
313 pub fn is_own(&self) -> bool {
315 match &self.kind {
316 EventTimelineItemKind::Local(_) => true,
317 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
318 }
319 }
320
321 pub fn is_editable(&self) -> bool {
323 if !self.is_own() {
327 return false;
329 }
330
331 match self.content() {
332 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
333 MsgLikeKind::Message(message) => match message.msgtype() {
334 MessageType::Text(_)
335 | MessageType::Emote(_)
336 | MessageType::Audio(_)
337 | MessageType::File(_)
338 | MessageType::Image(_)
339 | MessageType::Video(_) => true,
340 #[cfg(feature = "unstable-msc4274")]
341 MessageType::Gallery(_) => true,
342 _ => false,
343 },
344 MsgLikeKind::Poll(poll) => {
345 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
346 }
347 _ => false,
349 },
350 _ => {
351 false
353 }
354 }
355 }
356
357 pub fn is_highlighted(&self) -> bool {
359 match &self.kind {
360 EventTimelineItemKind::Local(_) => false,
361 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
362 }
363 }
364
365 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
367 match &self.kind {
368 EventTimelineItemKind::Local(_) => None,
369 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
370 }
371 }
372
373 pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
376 if !self.is_room_encrypted || self.is_local_echo() {
377 return TimelineEventShieldState::None;
378 }
379
380 if self.content().is_unable_to_decrypt() {
382 return TimelineEventShieldState::None;
383 }
384
385 if let Some(live_location) = self.content().as_live_location_state() {
396 return match live_location.latest_location() {
397 None => TimelineEventShieldState::None,
398 Some(beacon) => match beacon.encryption_info() {
399 Some(info) => {
400 if strict {
401 info.verification_state.to_shield_state_strict().into()
402 } else {
403 info.verification_state.to_shield_state_lax().into()
404 }
405 }
406 None => TimelineEventShieldState::Red {
407 code: TimelineEventShieldStateCode::SentInClear,
408 },
409 },
410 };
411 }
412
413 match self.encryption_info() {
414 Some(info) => {
415 if strict {
416 info.verification_state.to_shield_state_strict().into()
417 } else {
418 info.verification_state.to_shield_state_lax().into()
419 }
420 }
421 None => {
422 TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
423 }
424 }
425 }
426
427 pub fn can_be_replied_to(&self) -> bool {
429 if self.event_id().is_none() {
431 false
432 } else if self.content.is_message() {
433 true
434 } else if self.content().as_live_location_state().is_some() {
435 false
438 } else {
439 self.latest_json().is_some()
440 }
441 }
442
443 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
449 match &self.kind {
450 EventTimelineItemKind::Local(_) => None,
451 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
452 }
453 }
454
455 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
457 match &self.kind {
458 EventTimelineItemKind::Local(_) => None,
459 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
460 }
461 }
462
463 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
466 self.latest_edit_json().or_else(|| self.original_json())
467 }
468
469 pub fn origin(&self) -> Option<EventItemOrigin> {
473 match &self.kind {
474 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
475 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
476 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
477 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
478 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
479 RemoteEventOrigin::Unknown => None,
480 },
481 }
482 }
483
484 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
485 self.content = content;
486 }
487
488 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
490 Self { kind: kind.into(), ..self.clone() }
491 }
492
493 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
495 let mut new = self.clone();
496 new.content = new_content;
497 new
498 }
499
500 pub(super) fn with_content_and_latest_edit(
505 &self,
506 new_content: TimelineItemContent,
507 edit_json: Option<Raw<AnySyncTimelineEvent>>,
508 ) -> Self {
509 let mut new = self.clone();
510 new.content = new_content;
511 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
512 r.latest_edit_json = edit_json;
513 }
514 new
515 }
516
517 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
519 Self { sender_profile, ..self.clone() }
520 }
521
522 pub(super) fn with_encryption_info(
524 &self,
525 encryption_info: Option<Arc<EncryptionInfo>>,
526 ) -> Self {
527 let mut new = self.clone();
528 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
529 r.encryption_info = encryption_info;
530 }
531
532 new
533 }
534
535 pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
537 let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
538 content: self.content.clone(),
539 original_json: self.original_json().cloned(),
540 latest_edit_json: self.latest_edit_json().cloned(),
541 });
542 let content = self.content.redact(rules);
543 let kind = match &self.kind {
544 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
545 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
546 };
547 Self {
548 sender: self.sender.clone(),
549 sender_profile: self.sender_profile.clone(),
550 forwarder: self.forwarder.clone(),
551 forwarder_profile: self.forwarder_profile.clone(),
552 timestamp: self.timestamp,
553 content,
554 unredacted_item,
555 kind,
556 is_room_encrypted: self.is_room_encrypted,
557 }
558 }
559
560 pub(super) fn unredact(&self) -> Self {
564 let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
565 let kind = match &self.kind {
566 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
567 EventTimelineItemKind::Remote(r) => {
568 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
569 original_json: unredacted_item.original_json.clone(),
570 latest_edit_json: unredacted_item.latest_edit_json.clone(),
571 ..r.clone()
572 })
573 }
574 };
575 Self {
576 sender: self.sender.clone(),
577 sender_profile: self.sender_profile.clone(),
578 forwarder: self.forwarder.clone(),
579 forwarder_profile: self.forwarder_profile.clone(),
580 timestamp: self.timestamp,
581 content: unredacted_item.content.clone(),
582 unredacted_item: None,
583 kind,
584 is_room_encrypted: self.is_room_encrypted,
585 }
586 }
587
588 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
589 match &self.kind {
590 EventTimelineItemKind::Local(local) => {
591 if let Some(event_id) = local.event_id() {
592 TimelineItemHandle::Remote(event_id)
593 } else {
594 TimelineItemHandle::Local(
595 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
597 )
598 }
599 }
600 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
601 }
602 }
603
604 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
606 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
607 }
608
609 pub fn contains_only_emojis(&self) -> bool {
632 let body = match self.content() {
633 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
634 MsgLikeKind::Message(message) => match &message.msgtype {
635 MessageType::Text(text) => Some(text.body.as_str()),
636 MessageType::Audio(audio) => audio.caption(),
637 MessageType::File(file) => file.caption(),
638 MessageType::Image(image) => image.caption(),
639 MessageType::Video(video) => video.caption(),
640 _ => None,
641 },
642 MsgLikeKind::Sticker(_)
643 | MsgLikeKind::Poll(_)
644 | MsgLikeKind::Redacted
645 | MsgLikeKind::UnableToDecrypt(_)
646 | MsgLikeKind::Other(_)
647 | MsgLikeKind::LiveLocation(_) => None,
648 },
649 TimelineItemContent::MembershipChange(_)
650 | TimelineItemContent::ProfileChange(_)
651 | TimelineItemContent::OtherState(_)
652 | TimelineItemContent::FailedToParseMessageLike { .. }
653 | TimelineItemContent::FailedToParseState { .. }
654 | TimelineItemContent::CallInvite
655 | TimelineItemContent::RtcNotification { .. } => None,
656 };
657
658 if let Some(body) = body {
659 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
661
662 if graphemes.len() > 5 {
667 return false;
668 }
669
670 graphemes.iter().all(|g| emojis::get(g).is_some())
671 } else {
672 false
673 }
674 }
675}
676
677impl From<LocalEventTimelineItem> for EventTimelineItemKind {
678 fn from(value: LocalEventTimelineItem) -> Self {
679 EventTimelineItemKind::Local(value)
680 }
681}
682
683impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
684 fn from(value: RemoteEventTimelineItem) -> Self {
685 EventTimelineItemKind::Remote(value)
686 }
687}
688
689#[derive(Clone, Debug, Default, PartialEq, Eq)]
691pub struct Profile {
692 pub display_name: Option<String>,
694
695 pub display_name_ambiguous: bool,
701
702 pub avatar_url: Option<OwnedMxcUri>,
704}
705
706impl Profile {
707 pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
708 match room.get_member_no_sync(user_id).await {
709 Ok(Some(member)) => Some(Profile {
710 display_name: member.display_name().map(ToOwned::to_owned),
711 display_name_ambiguous: member.name_ambiguous(),
712 avatar_url: member.avatar_url().map(ToOwned::to_owned),
713 }),
714 Ok(None) if room.are_members_synced() => Some(Profile::default()),
715 Ok(None) => None,
716 Err(e) => {
717 error!(%user_id, "Failed to fetch room member information: {e}");
718 None
719 }
720 }
721 }
722}
723
724#[derive(Clone, Debug)]
728pub enum TimelineDetails<T> {
729 Unavailable,
732
733 Pending,
735
736 Ready(T),
738
739 Error(Arc<Error>),
741}
742
743impl<T> TimelineDetails<T> {
744 pub fn from_initial_value(value: Option<T>) -> Self {
750 match value {
751 Some(v) => Self::Ready(v),
752 None => Self::Unavailable,
753 }
754 }
755
756 pub fn is_unavailable(&self) -> bool {
757 matches!(self, Self::Unavailable)
758 }
759
760 pub fn is_ready(&self) -> bool {
761 matches!(self, Self::Ready(_))
762 }
763}
764
765#[derive(Clone, Copy, Debug)]
767#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
768pub enum EventItemOrigin {
769 Local,
771 Sync,
773 Pagination,
775 Cache,
777}
778
779#[derive(Clone, Debug)]
781pub enum ReactionStatus {
782 LocalToLocal(Option<SendReactionHandle>),
786 LocalToRemote(Option<SendHandle>),
790 RemoteToRemote(OwnedEventId),
794}
795
796#[derive(Clone, Debug)]
798pub struct ReactionInfo {
799 pub timestamp: MilliSecondsSinceUnixEpoch,
800 pub status: ReactionStatus,
802}
803
804#[derive(Debug, Clone, Default)]
809pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
810
811impl Deref for ReactionsByKeyBySender {
812 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
813
814 fn deref(&self) -> &Self::Target {
815 &self.0
816 }
817}
818
819impl DerefMut for ReactionsByKeyBySender {
820 fn deref_mut(&mut self) -> &mut Self::Target {
821 &mut self.0
822 }
823}
824
825impl ReactionsByKeyBySender {
826 pub(crate) fn remove_reaction(
832 &mut self,
833 sender: &UserId,
834 annotation: &str,
835 ) -> Option<ReactionInfo> {
836 if let Some(by_user) = self.0.get_mut(annotation)
837 && let Some(info) = by_user.swap_remove(sender)
838 {
839 if by_user.is_empty() {
841 self.0.swap_remove(annotation);
842 }
843 return Some(info);
844 }
845 None
846 }
847}
848
849#[derive(Clone, Copy, Debug, Eq, PartialEq)]
851pub enum TimelineEventShieldState {
852 Red {
855 code: TimelineEventShieldStateCode,
857 },
858 Grey {
861 code: TimelineEventShieldStateCode,
863 },
864 None,
866}
867
868impl From<ShieldState> for TimelineEventShieldState {
869 fn from(value: ShieldState) -> Self {
870 match value {
871 ShieldState::Red { code, message: _ } => {
872 TimelineEventShieldState::Red { code: code.into() }
873 }
874 ShieldState::Grey { code, message: _ } => {
875 TimelineEventShieldState::Grey { code: code.into() }
876 }
877 ShieldState::None => TimelineEventShieldState::None,
878 }
879 }
880}
881
882#[derive(Clone, Copy, Debug, Eq, PartialEq)]
884#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
885pub enum TimelineEventShieldStateCode {
886 AuthenticityNotGuaranteed,
888 UnknownDevice,
890 UnsignedDevice,
892 UnverifiedIdentity,
894 VerificationViolation,
896 MismatchedSender,
899 SentInClear,
901}
902
903impl From<ShieldStateCode> for TimelineEventShieldStateCode {
904 fn from(value: ShieldStateCode) -> Self {
905 use TimelineEventShieldStateCode::*;
906 match value {
907 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
908 ShieldStateCode::UnknownDevice => UnknownDevice,
909 ShieldStateCode::UnsignedDevice => UnsignedDevice,
910 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
911 ShieldStateCode::VerificationViolation => VerificationViolation,
912 ShieldStateCode::MismatchedSender => MismatchedSender,
913 }
914 }
915}
916
917#[cfg(test)]
918mod tests {
919 use std::time::Duration;
920
921 use ruma::{
922 MilliSecondsSinceUnixEpoch,
923 events::{
924 AnySyncTimelineEvent,
925 beacon_info::BeaconInfoEventContent,
926 room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
927 },
928 owned_event_id, owned_user_id,
929 serde::Raw,
930 uint,
931 };
932 use serde_json::json;
933
934 use super::{
935 EventSendState, EventTimelineItem, EventTimelineItemKind, LiveLocationState,
936 LocalEventTimelineItem, Message, MsgLikeContent, MsgLikeKind, RemoteEventOrigin,
937 RemoteEventTimelineItem, TimelineDetails, TimelineItemContent,
938 };
939
940 fn message_content() -> TimelineItemContent {
941 TimelineItemContent::MsgLike(MsgLikeContent {
942 kind: MsgLikeKind::Message(Message {
943 msgtype: MessageType::Text(TextMessageEventContent::plain("hello")),
944 edited: false,
945 mentions: None,
946 }),
947 reactions: Default::default(),
948 thread_root: None,
949 in_reply_to: None,
950 thread_summary: None,
951 })
952 }
953
954 fn live_location_content() -> TimelineItemContent {
955 TimelineItemContent::MsgLike(MsgLikeContent {
956 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(BeaconInfoEventContent::new(
957 None,
958 Duration::from_secs(300),
959 true,
960 Some(MilliSecondsSinceUnixEpoch(uint!(1))),
961 ))),
962 reactions: Default::default(),
963 thread_root: None,
964 in_reply_to: None,
965 thread_summary: None,
966 })
967 }
968
969 fn remote_item(
970 content: TimelineItemContent,
971 original_json: Option<Raw<AnySyncTimelineEvent>>,
972 ) -> EventTimelineItem {
973 EventTimelineItem::new(
974 owned_user_id!("@alice:example.org"),
975 TimelineDetails::Unavailable,
976 None,
977 None,
978 MilliSecondsSinceUnixEpoch(uint!(1)),
979 content,
980 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
981 event_id: owned_event_id!("$event"),
982 transaction_id: None,
983 read_receipts: Default::default(),
984 is_own: false,
985 is_highlighted: false,
986 encryption_info: None,
987 original_json,
988 latest_edit_json: None,
989 origin: RemoteEventOrigin::Sync,
990 }),
991 false,
992 )
993 }
994
995 fn local_unsent_item(content: TimelineItemContent) -> EventTimelineItem {
996 EventTimelineItem::new(
997 owned_user_id!("@alice:example.org"),
998 TimelineDetails::Unavailable,
999 None,
1000 None,
1001 MilliSecondsSinceUnixEpoch(uint!(1)),
1002 content,
1003 EventTimelineItemKind::Local(LocalEventTimelineItem {
1004 send_state: EventSendState::NotSentYet { progress: None },
1005 transaction_id: "t0".into(),
1006 send_handle: None,
1007 }),
1008 false,
1009 )
1010 }
1011
1012 fn sample_raw_event() -> Raw<AnySyncTimelineEvent> {
1013 Raw::from_json_string(
1014 json!({
1015 "content": RoomMessageEventContent::text_plain("hi"),
1016 "type": "m.room.message",
1017 "event_id": "$event",
1018 "room_id": "!room:example.org",
1019 "origin_server_ts": 1,
1020 "sender": "@alice:example.org",
1021 })
1022 .to_string(),
1023 )
1024 .unwrap()
1025 }
1026
1027 #[test]
1028 fn cannot_reply_to_local_unsent_events() {
1029 let item = local_unsent_item(message_content());
1030 assert!(!item.can_be_replied_to());
1031 }
1032
1033 #[test]
1034 fn can_reply_to_messages() {
1035 let item = remote_item(message_content(), None);
1036 assert!(item.can_be_replied_to());
1037 }
1038
1039 #[test]
1040 fn cannot_reply_to_live_location_events() {
1041 let item = remote_item(live_location_content(), Some(sample_raw_event()));
1042 assert!(!item.can_be_replied_to());
1043 }
1044
1045 #[test]
1046 fn cannot_reply_to_non_messages_with_no_json() {
1047 let item = remote_item(TimelineItemContent::CallInvite, None);
1048 assert!(!item.can_be_replied_to());
1049 }
1050
1051 #[test]
1052 fn can_reply_to_non_messages_with_json() {
1053 let item = remote_item(TimelineItemContent::CallInvite, Some(sample_raw_event()));
1054 assert!(item.can_be_replied_to());
1055 }
1056}