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;
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) kind: EventTimelineItemKind,
95 pub(super) is_room_encrypted: bool,
99}
100
101#[derive(Clone, Debug)]
102pub(super) enum EventTimelineItemKind {
103 Local(LocalEventTimelineItem),
105 Remote(RemoteEventTimelineItem),
107}
108
109#[derive(Clone, Debug, Eq, Hash, PartialEq)]
111pub enum TimelineEventItemId {
112 TransactionId(OwnedTransactionId),
115 EventId(OwnedEventId),
117}
118
119pub(crate) enum TimelineItemHandle<'a> {
125 Remote(&'a EventId),
126 Local(&'a SendHandle),
127}
128
129#[derive(Clone, Debug)]
132pub(super) struct UnredactedEventTimelineItem {
133 content: TimelineItemContent,
135
136 pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
138
139 pub(crate) latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
141}
142
143impl EventTimelineItem {
144 #[allow(clippy::too_many_arguments)]
145 pub(super) fn new(
146 sender: OwnedUserId,
147 sender_profile: TimelineDetails<Profile>,
148 forwarder: Option<OwnedUserId>,
149 forwarder_profile: Option<TimelineDetails<Profile>>,
150 timestamp: MilliSecondsSinceUnixEpoch,
151 content: TimelineItemContent,
152 kind: EventTimelineItemKind,
153 is_room_encrypted: bool,
154 ) -> Self {
155 Self {
156 sender,
157 sender_profile,
158 forwarder,
159 forwarder_profile,
160 timestamp,
161 content,
162 unredacted_item: None,
163 kind,
164 is_room_encrypted,
165 }
166 }
167
168 pub fn is_local_echo(&self) -> bool {
175 matches!(self.kind, EventTimelineItemKind::Local(_))
176 }
177
178 pub fn is_remote_event(&self) -> bool {
186 matches!(self.kind, EventTimelineItemKind::Remote(_))
187 }
188
189 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
191 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
192 }
193
194 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
196 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
197 }
198
199 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
202 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
203 }
204
205 pub fn send_state(&self) -> Option<&EventSendState> {
207 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
208 }
209
210 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
212 match &self.kind {
213 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
214 EventTimelineItemKind::Remote(_) => None,
215 }
216 }
217
218 pub fn identifier(&self) -> TimelineEventItemId {
224 match &self.kind {
225 EventTimelineItemKind::Local(local) => local.identifier(),
226 EventTimelineItemKind::Remote(remote) => {
227 TimelineEventItemId::EventId(remote.event_id.clone())
228 }
229 }
230 }
231
232 pub fn transaction_id(&self) -> Option<&TransactionId> {
237 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
238 }
239
240 pub fn event_id(&self) -> Option<&EventId> {
249 match &self.kind {
250 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
251 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
252 }
253 }
254
255 pub fn sender(&self) -> &UserId {
257 &self.sender
258 }
259
260 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
262 &self.sender_profile
263 }
264
265 pub fn forwarder(&self) -> Option<&UserId> {
270 self.forwarder.as_deref()
271 }
272
273 pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
278 self.forwarder_profile.as_ref()
279 }
280
281 pub fn content(&self) -> &TimelineItemContent {
283 &self.content
284 }
285
286 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
288 &mut self.content
289 }
290
291 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
298 static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
299 LazyLock::new(Default::default);
300 match &self.kind {
301 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
302 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
303 }
304 }
305
306 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
312 self.timestamp
313 }
314
315 pub fn is_own(&self) -> bool {
317 match &self.kind {
318 EventTimelineItemKind::Local(_) => true,
319 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
320 }
321 }
322
323 pub fn is_editable(&self) -> bool {
325 if !self.is_own() {
329 return false;
331 }
332
333 match self.content() {
334 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
335 MsgLikeKind::Message(message) => match message.msgtype() {
336 MessageType::Text(_)
337 | MessageType::Emote(_)
338 | MessageType::Audio(_)
339 | MessageType::File(_)
340 | MessageType::Image(_)
341 | MessageType::Video(_) => true,
342 #[cfg(feature = "unstable-msc4274")]
343 MessageType::Gallery(_) => true,
344 _ => false,
345 },
346 MsgLikeKind::Poll(poll) => {
347 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
348 }
349 _ => false,
351 },
352 _ => {
353 false
355 }
356 }
357 }
358
359 pub fn is_highlighted(&self) -> bool {
361 match &self.kind {
362 EventTimelineItemKind::Local(_) => false,
363 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
364 }
365 }
366
367 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
369 match &self.kind {
370 EventTimelineItemKind::Local(_) => None,
371 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
372 }
373 }
374
375 pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
378 if !self.is_room_encrypted || self.is_local_echo() {
379 return TimelineEventShieldState::None;
380 }
381
382 if self.content().is_unable_to_decrypt() {
384 return TimelineEventShieldState::None;
385 }
386
387 if let Some(live_location) = self.content().as_live_location_state() {
398 return match live_location.latest_location() {
399 None => TimelineEventShieldState::None,
400 Some(beacon) => match beacon.encryption_info() {
401 Some(info) => {
402 if strict {
403 info.verification_state.to_shield_state_strict().into()
404 } else {
405 info.verification_state.to_shield_state_lax().into()
406 }
407 }
408 None => TimelineEventShieldState::Red {
409 code: TimelineEventShieldStateCode::SentInClear,
410 },
411 },
412 };
413 }
414
415 match self.encryption_info() {
416 Some(info) => {
417 if strict {
418 info.verification_state.to_shield_state_strict().into()
419 } else {
420 info.verification_state.to_shield_state_lax().into()
421 }
422 }
423 None => {
424 TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
425 }
426 }
427 }
428
429 pub fn can_be_replied_to(&self) -> bool {
431 if self.event_id().is_none() {
433 false
434 } else if self.content.is_message() {
435 true
436 } else if self.content().as_live_location_state().is_some() {
437 false
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, is_local: bool) -> Self {
539 let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
540 content: self.content.clone(),
541 original_json: self.original_json().cloned(),
542 latest_edit_json: self.latest_edit_json().cloned(),
543 });
544 let content = self.content.redact(rules);
545 let kind = match &self.kind {
546 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
547 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
548 };
549 Self {
550 sender: self.sender.clone(),
551 sender_profile: self.sender_profile.clone(),
552 forwarder: self.forwarder.clone(),
553 forwarder_profile: self.forwarder_profile.clone(),
554 timestamp: self.timestamp,
555 content,
556 unredacted_item,
557 kind,
558 is_room_encrypted: self.is_room_encrypted,
559 }
560 }
561
562 pub(super) fn unredact(&self) -> Self {
566 let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
567 let kind = match &self.kind {
568 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
569 EventTimelineItemKind::Remote(r) => {
570 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
571 original_json: unredacted_item.original_json.clone(),
572 latest_edit_json: unredacted_item.latest_edit_json.clone(),
573 ..r.clone()
574 })
575 }
576 };
577 Self {
578 sender: self.sender.clone(),
579 sender_profile: self.sender_profile.clone(),
580 forwarder: self.forwarder.clone(),
581 forwarder_profile: self.forwarder_profile.clone(),
582 timestamp: self.timestamp,
583 content: unredacted_item.content.clone(),
584 unredacted_item: None,
585 kind,
586 is_room_encrypted: self.is_room_encrypted,
587 }
588 }
589
590 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
591 match &self.kind {
592 EventTimelineItemKind::Local(local) => {
593 if let Some(event_id) = local.event_id() {
594 TimelineItemHandle::Remote(event_id)
595 } else {
596 TimelineItemHandle::Local(
597 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
599 )
600 }
601 }
602 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
603 }
604 }
605
606 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
608 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
609 }
610
611 pub fn contains_only_emojis(&self) -> bool {
634 let body = match self.content() {
635 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
636 MsgLikeKind::Message(message) => match &message.msgtype {
637 MessageType::Text(text) => Some(text.body.as_str()),
638 MessageType::Audio(audio) => audio.caption(),
639 MessageType::File(file) => file.caption(),
640 MessageType::Image(image) => image.caption(),
641 MessageType::Video(video) => video.caption(),
642 _ => None,
643 },
644 MsgLikeKind::Sticker(_)
645 | MsgLikeKind::Poll(_)
646 | MsgLikeKind::Redacted
647 | MsgLikeKind::UnableToDecrypt(_)
648 | MsgLikeKind::Other(_)
649 | MsgLikeKind::LiveLocation(_) => None,
650 },
651 TimelineItemContent::MembershipChange(_)
652 | TimelineItemContent::ProfileChange(_)
653 | TimelineItemContent::OtherState(_)
654 | TimelineItemContent::FailedToParseMessageLike { .. }
655 | TimelineItemContent::FailedToParseState { .. }
656 | TimelineItemContent::CallInvite
657 | TimelineItemContent::RtcNotification { .. } => None,
658 };
659
660 if let Some(body) = body {
661 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
663
664 if graphemes.len() > 5 {
669 return false;
670 }
671
672 graphemes.iter().all(|g| emojis::get(g).is_some())
673 } else {
674 false
675 }
676 }
677}
678
679impl From<LocalEventTimelineItem> for EventTimelineItemKind {
680 fn from(value: LocalEventTimelineItem) -> Self {
681 EventTimelineItemKind::Local(value)
682 }
683}
684
685impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
686 fn from(value: RemoteEventTimelineItem) -> Self {
687 EventTimelineItemKind::Remote(value)
688 }
689}
690
691#[derive(Clone, Debug, Default, PartialEq, Eq)]
693pub struct Profile {
694 pub display_name: Option<String>,
696
697 pub display_name_ambiguous: bool,
703
704 pub avatar_url: Option<OwnedMxcUri>,
706
707 #[cfg(feature = "unstable-msc4426")]
709 pub status: Option<StatusProfileField>,
710
711 #[cfg(feature = "unstable-msc4426")]
713 pub call: Option<CallProfileField>,
714}
715
716impl Profile {
717 pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
718 match room.get_member_no_sync(user_id).await {
719 Ok(Some(member)) => Some(Profile {
720 display_name: member.display_name().map(ToOwned::to_owned),
721 display_name_ambiguous: member.name_ambiguous(),
722 avatar_url: member.avatar_url().map(ToOwned::to_owned),
723 #[cfg(feature = "unstable-msc4426")]
724 status: member.status().cloned(),
725 #[cfg(feature = "unstable-msc4426")]
726 call: member.call().cloned(),
727 }),
728 Ok(None) if room.are_members_synced() => Some(Profile::default()),
729 Ok(None) => None,
730 Err(e) => {
731 error!(%user_id, "Failed to fetch room member information: {e}");
732 None
733 }
734 }
735 }
736}
737
738#[derive(Clone, Debug)]
742pub enum TimelineDetails<T> {
743 Unavailable,
746
747 Pending,
749
750 Ready(T),
752
753 Error(Arc<Error>),
755}
756
757impl<T> TimelineDetails<T> {
758 pub fn from_initial_value(value: Option<T>) -> Self {
764 match value {
765 Some(v) => Self::Ready(v),
766 None => Self::Unavailable,
767 }
768 }
769
770 pub fn is_unavailable(&self) -> bool {
771 matches!(self, Self::Unavailable)
772 }
773
774 pub fn is_ready(&self) -> bool {
775 matches!(self, Self::Ready(_))
776 }
777}
778
779#[derive(Clone, Copy, Debug)]
781#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
782pub enum EventItemOrigin {
783 Local,
785 Sync,
787 Pagination,
789 Cache,
791}
792
793#[derive(Clone, Debug)]
795pub enum ReactionStatus {
796 LocalToLocal(Option<SendReactionHandle>),
800 LocalToRemote(Option<SendHandle>),
804 RemoteToRemote(OwnedEventId),
808}
809
810#[derive(Clone, Debug)]
812pub struct ReactionInfo {
813 pub timestamp: MilliSecondsSinceUnixEpoch,
814 pub status: ReactionStatus,
816}
817
818#[derive(Debug, Clone, Default)]
823pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
824
825impl Deref for ReactionsByKeyBySender {
826 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
827
828 fn deref(&self) -> &Self::Target {
829 &self.0
830 }
831}
832
833impl DerefMut for ReactionsByKeyBySender {
834 fn deref_mut(&mut self) -> &mut Self::Target {
835 &mut self.0
836 }
837}
838
839impl ReactionsByKeyBySender {
840 pub(crate) fn remove_reaction(
846 &mut self,
847 sender: &UserId,
848 annotation: &str,
849 ) -> Option<ReactionInfo> {
850 if let Some(by_user) = self.0.get_mut(annotation)
851 && let Some(info) = by_user.swap_remove(sender)
852 {
853 if by_user.is_empty() {
855 self.0.swap_remove(annotation);
856 }
857 return Some(info);
858 }
859 None
860 }
861}
862
863#[derive(Clone, Copy, Debug, Eq, PartialEq)]
865pub enum TimelineEventShieldState {
866 Red {
869 code: TimelineEventShieldStateCode,
871 },
872 Grey {
875 code: TimelineEventShieldStateCode,
877 },
878 None,
880}
881
882impl From<ShieldState> for TimelineEventShieldState {
883 fn from(value: ShieldState) -> Self {
884 match value {
885 ShieldState::Red { code, message: _ } => {
886 TimelineEventShieldState::Red { code: code.into() }
887 }
888 ShieldState::Grey { code, message: _ } => {
889 TimelineEventShieldState::Grey { code: code.into() }
890 }
891 ShieldState::None => TimelineEventShieldState::None,
892 }
893 }
894}
895
896#[derive(Clone, Copy, Debug, Eq, PartialEq)]
898#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
899pub enum TimelineEventShieldStateCode {
900 AuthenticityNotGuaranteed,
902 UnknownDevice,
904 UnsignedDevice,
906 UnverifiedIdentity,
908 VerificationViolation,
910 MismatchedSender,
913 SentInClear,
915}
916
917impl From<ShieldStateCode> for TimelineEventShieldStateCode {
918 fn from(value: ShieldStateCode) -> Self {
919 use TimelineEventShieldStateCode::*;
920 match value {
921 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
922 ShieldStateCode::UnknownDevice => UnknownDevice,
923 ShieldStateCode::UnsignedDevice => UnsignedDevice,
924 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
925 ShieldStateCode::VerificationViolation => VerificationViolation,
926 ShieldStateCode::MismatchedSender => MismatchedSender,
927 }
928 }
929}
930
931#[cfg(test)]
932mod tests {
933 use std::time::Duration;
934
935 use ruma::{
936 MilliSecondsSinceUnixEpoch,
937 events::{
938 AnySyncTimelineEvent,
939 beacon_info::BeaconInfoEventContent,
940 room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
941 },
942 owned_event_id, owned_user_id,
943 serde::Raw,
944 uint,
945 };
946 use serde_json::json;
947
948 use super::{
949 EventSendState, EventTimelineItem, EventTimelineItemKind, LiveLocationState,
950 LocalEventTimelineItem, Message, MsgLikeContent, MsgLikeKind, RemoteEventOrigin,
951 RemoteEventTimelineItem, TimelineDetails, TimelineItemContent,
952 };
953
954 fn message_content() -> TimelineItemContent {
955 TimelineItemContent::MsgLike(MsgLikeContent {
956 kind: MsgLikeKind::Message(Message {
957 msgtype: MessageType::Text(TextMessageEventContent::plain("hello")),
958 edited: false,
959 mentions: None,
960 }),
961 reactions: Default::default(),
962 thread_root: None,
963 in_reply_to: None,
964 thread_summary: None,
965 })
966 }
967
968 fn live_location_content() -> TimelineItemContent {
969 TimelineItemContent::MsgLike(MsgLikeContent {
970 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(BeaconInfoEventContent::new(
971 None,
972 Duration::from_secs(300),
973 true,
974 Some(MilliSecondsSinceUnixEpoch(uint!(1))),
975 ))),
976 reactions: Default::default(),
977 thread_root: None,
978 in_reply_to: None,
979 thread_summary: None,
980 })
981 }
982
983 fn remote_item(
984 content: TimelineItemContent,
985 original_json: Option<Raw<AnySyncTimelineEvent>>,
986 ) -> EventTimelineItem {
987 EventTimelineItem::new(
988 owned_user_id!("@alice:example.org"),
989 TimelineDetails::Unavailable,
990 None,
991 None,
992 MilliSecondsSinceUnixEpoch(uint!(1)),
993 content,
994 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
995 event_id: owned_event_id!("$event"),
996 transaction_id: None,
997 read_receipts: Default::default(),
998 is_own: false,
999 is_highlighted: false,
1000 encryption_info: None,
1001 original_json,
1002 latest_edit_json: None,
1003 origin: RemoteEventOrigin::Sync,
1004 }),
1005 false,
1006 )
1007 }
1008
1009 fn local_unsent_item(content: TimelineItemContent) -> EventTimelineItem {
1010 EventTimelineItem::new(
1011 owned_user_id!("@alice:example.org"),
1012 TimelineDetails::Unavailable,
1013 None,
1014 None,
1015 MilliSecondsSinceUnixEpoch(uint!(1)),
1016 content,
1017 EventTimelineItemKind::Local(LocalEventTimelineItem {
1018 send_state: EventSendState::NotSentYet { progress: None },
1019 transaction_id: "t0".into(),
1020 send_handle: None,
1021 }),
1022 false,
1023 )
1024 }
1025
1026 fn sample_raw_event() -> Raw<AnySyncTimelineEvent> {
1027 Raw::from_json_string(
1028 json!({
1029 "content": RoomMessageEventContent::text_plain("hi"),
1030 "type": "m.room.message",
1031 "event_id": "$event",
1032 "room_id": "!room:example.org",
1033 "origin_server_ts": 1,
1034 "sender": "@alice:example.org",
1035 })
1036 .to_string(),
1037 )
1038 .unwrap()
1039 }
1040
1041 #[test]
1042 fn cannot_reply_to_local_unsent_events() {
1043 let item = local_unsent_item(message_content());
1044 assert!(!item.can_be_replied_to());
1045 }
1046
1047 #[test]
1048 fn can_reply_to_messages() {
1049 let item = remote_item(message_content(), None);
1050 assert!(item.can_be_replied_to());
1051 }
1052
1053 #[test]
1054 fn cannot_reply_to_live_location_events() {
1055 let item = remote_item(live_location_content(), Some(sample_raw_event()));
1056 assert!(!item.can_be_replied_to());
1057 }
1058
1059 #[test]
1060 fn cannot_reply_to_non_messages_with_no_json() {
1061 let item = remote_item(TimelineItemContent::CallInvite, None);
1062 assert!(!item.can_be_replied_to());
1063 }
1064
1065 #[test]
1066 fn can_reply_to_non_messages_with_json() {
1067 let item = remote_item(TimelineItemContent::CallInvite, Some(sample_raw_event()));
1068 assert!(item.can_be_replied_to());
1069 }
1070}