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)]
134pub struct EditRevision {
135 pub content: TimelineItemContent,
137 pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
139}
140
141#[derive(Clone, Debug)]
144pub(super) struct UnredactedEventTimelineItem {
145 content: TimelineItemContent,
147
148 pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
150
151 pub(crate) latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
153}
154
155impl EventTimelineItem {
156 #[allow(clippy::too_many_arguments)]
157 pub(super) fn new(
158 sender: OwnedUserId,
159 sender_profile: TimelineDetails<Profile>,
160 forwarder: Option<OwnedUserId>,
161 forwarder_profile: Option<TimelineDetails<Profile>>,
162 timestamp: MilliSecondsSinceUnixEpoch,
163 content: TimelineItemContent,
164 kind: EventTimelineItemKind,
165 is_room_encrypted: bool,
166 ) -> Self {
167 Self {
168 sender,
169 sender_profile,
170 forwarder,
171 forwarder_profile,
172 timestamp,
173 content,
174 unredacted_item: None,
175 kind,
176 is_room_encrypted,
177 }
178 }
179
180 pub fn is_local_echo(&self) -> bool {
187 matches!(self.kind, EventTimelineItemKind::Local(_))
188 }
189
190 pub fn is_remote_event(&self) -> bool {
198 matches!(self.kind, EventTimelineItemKind::Remote(_))
199 }
200
201 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
203 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
204 }
205
206 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
208 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
209 }
210
211 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
214 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
215 }
216
217 pub fn send_state(&self) -> Option<&EventSendState> {
219 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
220 }
221
222 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
224 match &self.kind {
225 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
226 EventTimelineItemKind::Remote(_) => None,
227 }
228 }
229
230 pub fn identifier(&self) -> TimelineEventItemId {
236 match &self.kind {
237 EventTimelineItemKind::Local(local) => local.identifier(),
238 EventTimelineItemKind::Remote(remote) => {
239 TimelineEventItemId::EventId(remote.event_id.clone())
240 }
241 }
242 }
243
244 pub fn transaction_id(&self) -> Option<&TransactionId> {
249 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
250 }
251
252 pub fn event_id(&self) -> Option<&EventId> {
261 match &self.kind {
262 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
263 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
264 }
265 }
266
267 pub fn sender(&self) -> &UserId {
269 &self.sender
270 }
271
272 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
274 &self.sender_profile
275 }
276
277 pub fn forwarder(&self) -> Option<&UserId> {
282 self.forwarder.as_deref()
283 }
284
285 pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
290 self.forwarder_profile.as_ref()
291 }
292
293 pub fn content(&self) -> &TimelineItemContent {
295 &self.content
296 }
297
298 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
300 &mut self.content
301 }
302
303 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
310 static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
311 LazyLock::new(Default::default);
312 match &self.kind {
313 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
314 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
315 }
316 }
317
318 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
324 self.timestamp
325 }
326
327 pub fn is_own(&self) -> bool {
329 match &self.kind {
330 EventTimelineItemKind::Local(_) => true,
331 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
332 }
333 }
334
335 pub fn is_editable(&self) -> bool {
337 if !self.is_own() {
341 return false;
343 }
344
345 match self.content() {
346 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
347 MsgLikeKind::Message(message) => match message.msgtype() {
348 MessageType::Text(_)
349 | MessageType::Emote(_)
350 | MessageType::Audio(_)
351 | MessageType::File(_)
352 | MessageType::Image(_)
353 | MessageType::Video(_) => true,
354 #[cfg(feature = "unstable-msc4274")]
355 MessageType::Gallery(_) => true,
356 _ => false,
357 },
358 MsgLikeKind::Poll(poll) => {
359 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
360 }
361 _ => false,
363 },
364 _ => {
365 false
367 }
368 }
369 }
370
371 pub fn is_highlighted(&self) -> bool {
373 match &self.kind {
374 EventTimelineItemKind::Local(_) => false,
375 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
376 }
377 }
378
379 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
381 match &self.kind {
382 EventTimelineItemKind::Local(_) => None,
383 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
384 }
385 }
386
387 pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
390 if !self.is_room_encrypted || self.is_local_echo() {
391 return TimelineEventShieldState::None;
392 }
393
394 if self.content().is_unable_to_decrypt() {
396 return TimelineEventShieldState::None;
397 }
398
399 if let Some(live_location) = self.content().as_live_location_state() {
410 return match live_location.latest_location() {
411 None => TimelineEventShieldState::None,
412 Some(beacon) => match beacon.encryption_info() {
413 Some(info) => {
414 if strict {
415 info.verification_state.to_shield_state_strict().into()
416 } else {
417 info.verification_state.to_shield_state_lax().into()
418 }
419 }
420 None => TimelineEventShieldState::Red {
421 code: TimelineEventShieldStateCode::SentInClear,
422 },
423 },
424 };
425 }
426
427 match self.encryption_info() {
428 Some(info) => {
429 if strict {
430 info.verification_state.to_shield_state_strict().into()
431 } else {
432 info.verification_state.to_shield_state_lax().into()
433 }
434 }
435 None => {
436 TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
437 }
438 }
439 }
440
441 pub fn can_be_replied_to(&self) -> bool {
443 if self.event_id().is_none() {
445 false
446 } else if self.content.is_message() {
447 true
448 } else if self.content().as_live_location_state().is_some() {
449 false
452 } else {
453 self.latest_json().is_some()
454 }
455 }
456
457 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
463 match &self.kind {
464 EventTimelineItemKind::Local(_) => None,
465 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
466 }
467 }
468
469 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
471 match &self.kind {
472 EventTimelineItemKind::Local(_) => None,
473 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
474 }
475 }
476
477 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
480 self.latest_edit_json().or_else(|| self.original_json())
481 }
482
483 pub fn origin(&self) -> Option<EventItemOrigin> {
487 match &self.kind {
488 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
489 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
490 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
491 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
492 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
493 RemoteEventOrigin::Unknown => None,
494 },
495 }
496 }
497
498 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
499 self.content = content;
500 }
501
502 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
504 Self { kind: kind.into(), ..self.clone() }
505 }
506
507 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
509 let mut new = self.clone();
510 new.content = new_content;
511 new
512 }
513
514 pub(super) fn with_content_and_latest_edit(
519 &self,
520 new_content: TimelineItemContent,
521 edit_json: Option<Raw<AnySyncTimelineEvent>>,
522 ) -> Self {
523 let mut new = self.clone();
524 new.content = new_content;
525 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
526 r.latest_edit_json = edit_json;
527 }
528 new
529 }
530
531 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
533 Self { sender_profile, ..self.clone() }
534 }
535
536 pub(super) fn with_encryption_info(
538 &self,
539 encryption_info: Option<Arc<EncryptionInfo>>,
540 ) -> Self {
541 let mut new = self.clone();
542 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
543 r.encryption_info = encryption_info;
544 }
545
546 new
547 }
548
549 pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
551 let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
552 content: self.content.clone(),
553 original_json: self.original_json().cloned(),
554 latest_edit_json: self.latest_edit_json().cloned(),
555 });
556 let content = self.content.redact(rules);
557 let kind = match &self.kind {
558 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
559 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
560 };
561 Self {
562 sender: self.sender.clone(),
563 sender_profile: self.sender_profile.clone(),
564 forwarder: self.forwarder.clone(),
565 forwarder_profile: self.forwarder_profile.clone(),
566 timestamp: self.timestamp,
567 content,
568 unredacted_item,
569 kind,
570 is_room_encrypted: self.is_room_encrypted,
571 }
572 }
573
574 pub(super) fn unredact(&self) -> Self {
578 let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
579 let kind = match &self.kind {
580 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
581 EventTimelineItemKind::Remote(r) => {
582 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
583 original_json: unredacted_item.original_json.clone(),
584 latest_edit_json: unredacted_item.latest_edit_json.clone(),
585 ..r.clone()
586 })
587 }
588 };
589 Self {
590 sender: self.sender.clone(),
591 sender_profile: self.sender_profile.clone(),
592 forwarder: self.forwarder.clone(),
593 forwarder_profile: self.forwarder_profile.clone(),
594 timestamp: self.timestamp,
595 content: unredacted_item.content.clone(),
596 unredacted_item: None,
597 kind,
598 is_room_encrypted: self.is_room_encrypted,
599 }
600 }
601
602 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
603 match &self.kind {
604 EventTimelineItemKind::Local(local) => {
605 if let Some(event_id) = local.event_id() {
606 TimelineItemHandle::Remote(event_id)
607 } else {
608 TimelineItemHandle::Local(
609 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
611 )
612 }
613 }
614 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
615 }
616 }
617
618 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
620 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
621 }
622
623 pub fn contains_only_emojis(&self) -> bool {
646 let body = match self.content() {
647 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
648 MsgLikeKind::Message(message) => match &message.msgtype {
649 MessageType::Text(text) => Some(text.body.as_str()),
650 MessageType::Audio(audio) => audio.caption(),
651 MessageType::File(file) => file.caption(),
652 MessageType::Image(image) => image.caption(),
653 MessageType::Video(video) => video.caption(),
654 _ => None,
655 },
656 MsgLikeKind::Sticker(_)
657 | MsgLikeKind::Poll(_)
658 | MsgLikeKind::Redacted
659 | MsgLikeKind::UnableToDecrypt(_)
660 | MsgLikeKind::Other(_)
661 | MsgLikeKind::LiveLocation(_) => None,
662 },
663 TimelineItemContent::MembershipChange(_)
664 | TimelineItemContent::ProfileChange(_)
665 | TimelineItemContent::OtherState(_)
666 | TimelineItemContent::FailedToParseMessageLike { .. }
667 | TimelineItemContent::FailedToParseState { .. }
668 | TimelineItemContent::CallInvite
669 | TimelineItemContent::RtcNotification { .. } => None,
670 };
671
672 if let Some(body) = body {
673 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
675
676 if graphemes.len() > 5 {
681 return false;
682 }
683
684 graphemes.iter().all(|g| emojis::get(g).is_some())
685 } else {
686 false
687 }
688 }
689}
690
691impl From<LocalEventTimelineItem> for EventTimelineItemKind {
692 fn from(value: LocalEventTimelineItem) -> Self {
693 EventTimelineItemKind::Local(value)
694 }
695}
696
697impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
698 fn from(value: RemoteEventTimelineItem) -> Self {
699 EventTimelineItemKind::Remote(value)
700 }
701}
702
703#[derive(Clone, Debug, Default, PartialEq, Eq)]
705pub struct Profile {
706 pub display_name: Option<String>,
708
709 pub display_name_ambiguous: bool,
715
716 pub avatar_url: Option<OwnedMxcUri>,
718
719 #[cfg(feature = "unstable-msc4426")]
721 pub status: Option<StatusProfileField>,
722
723 #[cfg(feature = "unstable-msc4426")]
725 pub call: Option<CallProfileField>,
726}
727
728impl Profile {
729 pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
730 match room.get_member_no_sync(user_id).await {
731 Ok(Some(member)) => Some(Profile {
732 display_name: member.display_name().map(ToOwned::to_owned),
733 display_name_ambiguous: member.name_ambiguous(),
734 avatar_url: member.avatar_url().map(ToOwned::to_owned),
735 #[cfg(feature = "unstable-msc4426")]
736 status: member.status().cloned(),
737 #[cfg(feature = "unstable-msc4426")]
738 call: member.call().cloned(),
739 }),
740 Ok(None) if room.are_members_synced() => Some(Profile::default()),
741 Ok(None) => None,
742 Err(e) => {
743 error!(%user_id, "Failed to fetch room member information: {e}");
744 None
745 }
746 }
747 }
748}
749
750#[derive(Clone, Debug)]
754pub enum TimelineDetails<T> {
755 Unavailable,
758
759 Pending,
761
762 Ready(T),
764
765 Error(Arc<Error>),
767}
768
769impl<T> TimelineDetails<T> {
770 pub fn from_initial_value(value: Option<T>) -> Self {
776 match value {
777 Some(v) => Self::Ready(v),
778 None => Self::Unavailable,
779 }
780 }
781
782 pub fn is_unavailable(&self) -> bool {
783 matches!(self, Self::Unavailable)
784 }
785
786 pub fn is_ready(&self) -> bool {
787 matches!(self, Self::Ready(_))
788 }
789}
790
791#[derive(Clone, Copy, Debug)]
793#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
794pub enum EventItemOrigin {
795 Local,
797 Sync,
799 Pagination,
801 Cache,
803}
804
805#[derive(Clone, Debug)]
807pub enum ReactionStatus {
808 LocalToLocal(Option<SendReactionHandle>),
812 LocalToRemote(Option<SendHandle>),
816 RemoteToRemote(OwnedEventId),
820}
821
822#[derive(Clone, Debug)]
824pub struct ReactionInfo {
825 pub timestamp: MilliSecondsSinceUnixEpoch,
826 pub status: ReactionStatus,
828}
829
830#[derive(Debug, Clone, Default)]
835pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
836
837impl Deref for ReactionsByKeyBySender {
838 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
839
840 fn deref(&self) -> &Self::Target {
841 &self.0
842 }
843}
844
845impl DerefMut for ReactionsByKeyBySender {
846 fn deref_mut(&mut self) -> &mut Self::Target {
847 &mut self.0
848 }
849}
850
851impl ReactionsByKeyBySender {
852 pub(crate) fn remove_reaction(
858 &mut self,
859 sender: &UserId,
860 annotation: &str,
861 ) -> Option<ReactionInfo> {
862 if let Some(by_user) = self.0.get_mut(annotation)
863 && let Some(info) = by_user.swap_remove(sender)
864 {
865 if by_user.is_empty() {
867 self.0.swap_remove(annotation);
868 }
869 return Some(info);
870 }
871 None
872 }
873}
874
875#[derive(Clone, Copy, Debug, Eq, PartialEq)]
877pub enum TimelineEventShieldState {
878 Red {
881 code: TimelineEventShieldStateCode,
883 },
884 Grey {
887 code: TimelineEventShieldStateCode,
889 },
890 None,
892}
893
894impl From<ShieldState> for TimelineEventShieldState {
895 fn from(value: ShieldState) -> Self {
896 match value {
897 ShieldState::Red { code, message: _ } => {
898 TimelineEventShieldState::Red { code: code.into() }
899 }
900 ShieldState::Grey { code, message: _ } => {
901 TimelineEventShieldState::Grey { code: code.into() }
902 }
903 ShieldState::None => TimelineEventShieldState::None,
904 }
905 }
906}
907
908#[derive(Clone, Copy, Debug, Eq, PartialEq)]
910#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
911pub enum TimelineEventShieldStateCode {
912 AuthenticityNotGuaranteed,
914 UnknownDevice,
916 UnsignedDevice,
918 UnverifiedIdentity,
920 VerificationViolation,
922 MismatchedSender,
925 SentInClear,
927}
928
929impl From<ShieldStateCode> for TimelineEventShieldStateCode {
930 fn from(value: ShieldStateCode) -> Self {
931 use TimelineEventShieldStateCode::*;
932 match value {
933 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
934 ShieldStateCode::UnknownDevice => UnknownDevice,
935 ShieldStateCode::UnsignedDevice => UnsignedDevice,
936 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
937 ShieldStateCode::VerificationViolation => VerificationViolation,
938 ShieldStateCode::MismatchedSender => MismatchedSender,
939 }
940 }
941}
942
943#[cfg(test)]
944mod tests {
945 use std::time::Duration;
946
947 use ruma::{
948 MilliSecondsSinceUnixEpoch,
949 events::{
950 AnySyncTimelineEvent,
951 beacon_info::BeaconInfoEventContent,
952 room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
953 },
954 owned_event_id, owned_user_id,
955 serde::Raw,
956 uint,
957 };
958 use serde_json::json;
959
960 use super::{
961 EventSendState, EventTimelineItem, EventTimelineItemKind, LiveLocationState,
962 LocalEventTimelineItem, Message, MsgLikeContent, MsgLikeKind, RemoteEventOrigin,
963 RemoteEventTimelineItem, TimelineDetails, TimelineItemContent,
964 };
965
966 fn message_content() -> TimelineItemContent {
967 TimelineItemContent::MsgLike(MsgLikeContent {
968 kind: MsgLikeKind::Message(Message {
969 msgtype: MessageType::Text(TextMessageEventContent::plain("hello")),
970 edited: false,
971 mentions: None,
972 }),
973 reactions: Default::default(),
974 thread_root: None,
975 in_reply_to: None,
976 thread_summary: None,
977 })
978 }
979
980 fn live_location_content() -> TimelineItemContent {
981 TimelineItemContent::MsgLike(MsgLikeContent {
982 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(BeaconInfoEventContent::new(
983 None,
984 Duration::from_secs(300),
985 true,
986 Some(MilliSecondsSinceUnixEpoch(uint!(1))),
987 ))),
988 reactions: Default::default(),
989 thread_root: None,
990 in_reply_to: None,
991 thread_summary: None,
992 })
993 }
994
995 fn remote_item(
996 content: TimelineItemContent,
997 original_json: Option<Raw<AnySyncTimelineEvent>>,
998 ) -> EventTimelineItem {
999 EventTimelineItem::new(
1000 owned_user_id!("@alice:example.org"),
1001 TimelineDetails::Unavailable,
1002 None,
1003 None,
1004 MilliSecondsSinceUnixEpoch(uint!(1)),
1005 content,
1006 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
1007 event_id: owned_event_id!("$event"),
1008 transaction_id: None,
1009 read_receipts: Default::default(),
1010 is_own: false,
1011 is_highlighted: false,
1012 encryption_info: None,
1013 original_json,
1014 latest_edit_json: None,
1015 origin: RemoteEventOrigin::Sync,
1016 }),
1017 false,
1018 )
1019 }
1020
1021 fn local_unsent_item(content: TimelineItemContent) -> EventTimelineItem {
1022 EventTimelineItem::new(
1023 owned_user_id!("@alice:example.org"),
1024 TimelineDetails::Unavailable,
1025 None,
1026 None,
1027 MilliSecondsSinceUnixEpoch(uint!(1)),
1028 content,
1029 EventTimelineItemKind::Local(LocalEventTimelineItem {
1030 send_state: EventSendState::NotSentYet { progress: None },
1031 transaction_id: "t0".into(),
1032 send_handle: None,
1033 }),
1034 false,
1035 )
1036 }
1037
1038 fn sample_raw_event() -> Raw<AnySyncTimelineEvent> {
1039 Raw::from_json_string(
1040 json!({
1041 "content": RoomMessageEventContent::text_plain("hi"),
1042 "type": "m.room.message",
1043 "event_id": "$event",
1044 "room_id": "!room:example.org",
1045 "origin_server_ts": 1,
1046 "sender": "@alice:example.org",
1047 })
1048 .to_string(),
1049 )
1050 .unwrap()
1051 }
1052
1053 #[test]
1054 fn cannot_reply_to_local_unsent_events() {
1055 let item = local_unsent_item(message_content());
1056 assert!(!item.can_be_replied_to());
1057 }
1058
1059 #[test]
1060 fn can_reply_to_messages() {
1061 let item = remote_item(message_content(), None);
1062 assert!(item.can_be_replied_to());
1063 }
1064
1065 #[test]
1066 fn cannot_reply_to_live_location_events() {
1067 let item = remote_item(live_location_content(), Some(sample_raw_event()));
1068 assert!(!item.can_be_replied_to());
1069 }
1070
1071 #[test]
1072 fn cannot_reply_to_non_messages_with_no_json() {
1073 let item = remote_item(TimelineItemContent::CallInvite, None);
1074 assert!(!item.can_be_replied_to());
1075 }
1076
1077 #[test]
1078 fn can_reply_to_non_messages_with_json() {
1079 let item = remote_item(TimelineItemContent::CallInvite, Some(sample_raw_event()));
1080 assert!(item.can_be_replied_to());
1081 }
1082}