1use std::{
16 ops::{Deref, DerefMut},
17 sync::{Arc, LazyLock},
18};
19
20use as_variant::as_variant;
21use indexmap::IndexMap;
22use matrix_sdk::{
23 Error, Room,
24 deserialized_responses::{EncryptionInfo, ShieldState},
25 send_queue::SendHandle,
26};
27use matrix_sdk_base::deserialized_responses::ShieldStateCode;
28#[cfg(feature = "unstable-msc4426")]
29use ruma::profile::{CallProfileField, StatusProfileField};
30use ruma::{
31 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
32 OwnedUserId, TransactionId, UserId,
33 events::{AnySyncTimelineEvent, receipt::Receipt, room::message::MessageType},
34 room_version_rules::RedactionRules,
35 serde::Raw,
36};
37use tracing::error;
38use unicode_segmentation::UnicodeSegmentation;
39
40mod content;
41mod local;
42mod remote;
43
44pub use self::{
45 content::{
46 AnyOtherStateEventContentChange, BeaconInfo, EmbeddedEvent, EncryptedMessage,
47 InReplyToDetails, LiveLocationState, MemberProfileChange, MembershipChange, Message,
48 MsgLikeContent, MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState,
49 RoomMembershipChange, RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
50 },
51 local::{EventSendState, MediaUploadProgress},
52};
53pub(super) use self::{
54 content::{
55 beacon_info_matches, extract_bundled_edit_event_json, extract_poll_edit_content,
56 extract_room_msg_edit_content,
57 },
58 local::LocalEventTimelineItem,
59 remote::{RemoteEventOrigin, RemoteEventTimelineItem},
60};
61
62#[derive(Clone, Debug)]
68pub struct EventTimelineItem {
69 pub(super) sender: OwnedUserId,
71 pub(super) sender_profile: TimelineDetails<Profile>,
73 pub(super) forwarder: Option<OwnedUserId>,
78 pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
83 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
85 pub(super) content: TimelineItemContent,
88 pub(super) unredacted_item: Option<UnredactedEventTimelineItem>,
93 pub(super) redaction_send_state: Option<EventSendState>,
95 pub(super) edit_send_state: Option<EventSendState>,
97 pub(super) kind: EventTimelineItemKind,
99 pub(super) is_room_encrypted: bool,
103}
104
105#[derive(Clone, Debug)]
106pub(super) enum EventTimelineItemKind {
107 Local(LocalEventTimelineItem),
109 Remote(RemoteEventTimelineItem),
111}
112
113#[derive(Clone, Debug, Eq, Hash, PartialEq)]
115pub enum TimelineEventItemId {
116 TransactionId(OwnedTransactionId),
119 EventId(OwnedEventId),
121}
122
123pub(crate) enum TimelineItemHandle<'a> {
129 Remote(&'a EventId),
130 Local(&'a SendHandle),
131}
132
133#[derive(Clone, Debug)]
138pub struct EditRevision {
139 pub content: TimelineItemContent,
141 pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
143}
144
145#[derive(Clone, Debug)]
148pub(super) struct UnredactedEventTimelineItem {
149 content: TimelineItemContent,
151
152 pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
154
155 pub(crate) latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
157}
158
159impl EventTimelineItem {
160 #[allow(clippy::too_many_arguments)]
161 pub(super) fn new(
162 sender: OwnedUserId,
163 sender_profile: TimelineDetails<Profile>,
164 forwarder: Option<OwnedUserId>,
165 forwarder_profile: Option<TimelineDetails<Profile>>,
166 timestamp: MilliSecondsSinceUnixEpoch,
167 content: TimelineItemContent,
168 kind: EventTimelineItemKind,
169 is_room_encrypted: bool,
170 ) -> Self {
171 Self {
172 sender,
173 sender_profile,
174 forwarder,
175 forwarder_profile,
176 timestamp,
177 content,
178 unredacted_item: None,
179 redaction_send_state: None,
180 edit_send_state: None,
181 kind,
182 is_room_encrypted,
183 }
184 }
185
186 pub fn is_local_echo(&self) -> bool {
193 matches!(self.kind, EventTimelineItemKind::Local(_))
194 }
195
196 pub fn is_remote_event(&self) -> bool {
204 matches!(self.kind, EventTimelineItemKind::Remote(_))
205 }
206
207 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
209 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
210 }
211
212 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
214 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
215 }
216
217 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
220 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
221 }
222
223 pub fn send_state(&self) -> Option<&EventSendState> {
225 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
226 }
227
228 pub fn redaction_send_state(&self) -> Option<&EventSendState> {
231 self.redaction_send_state.as_ref()
232 }
233
234 pub fn edit_send_state(&self) -> Option<&EventSendState> {
238 self.edit_send_state.as_ref()
239 }
240
241 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
243 match &self.kind {
244 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
245 EventTimelineItemKind::Remote(_) => None,
246 }
247 }
248
249 pub fn identifier(&self) -> TimelineEventItemId {
255 match &self.kind {
256 EventTimelineItemKind::Local(local) => local.identifier(),
257 EventTimelineItemKind::Remote(remote) => {
258 TimelineEventItemId::EventId(remote.event_id.clone())
259 }
260 }
261 }
262
263 pub fn transaction_id(&self) -> Option<&TransactionId> {
268 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
269 }
270
271 pub fn event_id(&self) -> Option<&EventId> {
280 match &self.kind {
281 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
282 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
283 }
284 }
285
286 pub fn sender(&self) -> &UserId {
288 &self.sender
289 }
290
291 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
293 &self.sender_profile
294 }
295
296 pub fn forwarder(&self) -> Option<&UserId> {
301 self.forwarder.as_deref()
302 }
303
304 pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
309 self.forwarder_profile.as_ref()
310 }
311
312 pub fn content(&self) -> &TimelineItemContent {
314 &self.content
315 }
316
317 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
319 &mut self.content
320 }
321
322 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
329 static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
330 LazyLock::new(Default::default);
331 match &self.kind {
332 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
333 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
334 }
335 }
336
337 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
343 self.timestamp
344 }
345
346 pub fn is_own(&self) -> bool {
348 match &self.kind {
349 EventTimelineItemKind::Local(_) => true,
350 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
351 }
352 }
353
354 pub fn is_editable(&self) -> bool {
356 if !self.is_own() {
360 return false;
362 }
363
364 match self.content() {
365 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
366 MsgLikeKind::Message(message) => match message.msgtype() {
367 MessageType::Text(_)
368 | MessageType::Emote(_)
369 | MessageType::Audio(_)
370 | MessageType::File(_)
371 | MessageType::Image(_)
372 | MessageType::Video(_) => true,
373 #[cfg(feature = "unstable-msc4274")]
374 MessageType::Gallery(_) => true,
375 _ => false,
376 },
377 MsgLikeKind::Poll(poll) => {
378 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
379 }
380 _ => false,
382 },
383 _ => {
384 false
386 }
387 }
388 }
389
390 pub fn is_highlighted(&self) -> bool {
392 match &self.kind {
393 EventTimelineItemKind::Local(_) => false,
394 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
395 }
396 }
397
398 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
400 match &self.kind {
401 EventTimelineItemKind::Local(_) => None,
402 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
403 }
404 }
405
406 pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
409 if !self.is_room_encrypted || self.is_local_echo() {
410 return TimelineEventShieldState::None;
411 }
412
413 if self.content().is_unable_to_decrypt() {
415 return TimelineEventShieldState::None;
416 }
417
418 if let Some(live_location) = self.content().as_live_location_state() {
429 return match live_location.latest_location() {
430 None => TimelineEventShieldState::None,
431 Some(beacon) => match beacon.encryption_info() {
432 Some(info) => {
433 if strict {
434 info.verification_state.to_shield_state_strict().into()
435 } else {
436 info.verification_state.to_shield_state_lax().into()
437 }
438 }
439 None => TimelineEventShieldState::Red {
440 code: TimelineEventShieldStateCode::SentInClear,
441 },
442 },
443 };
444 }
445
446 match self.encryption_info() {
447 Some(info) => {
448 if strict {
449 info.verification_state.to_shield_state_strict().into()
450 } else {
451 info.verification_state.to_shield_state_lax().into()
452 }
453 }
454 None => {
455 TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
456 }
457 }
458 }
459
460 pub fn can_be_replied_to(&self) -> bool {
462 if self.event_id().is_none() {
464 false
465 } else if self.content.is_message() {
466 true
467 } else if self.content().as_live_location_state().is_some() {
468 false
471 } else {
472 self.latest_json().is_some()
473 }
474 }
475
476 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
482 match &self.kind {
483 EventTimelineItemKind::Local(_) => None,
484 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
485 }
486 }
487
488 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
490 match &self.kind {
491 EventTimelineItemKind::Local(_) => None,
492 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
493 }
494 }
495
496 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
499 self.latest_edit_json().or_else(|| self.original_json())
500 }
501
502 pub fn origin(&self) -> Option<EventItemOrigin> {
506 match &self.kind {
507 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
508 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
509 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
510 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
511 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
512 RemoteEventOrigin::Unknown => None,
513 },
514 }
515 }
516
517 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
518 self.content = content;
519 }
520
521 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
523 Self { kind: kind.into(), ..self.clone() }
524 }
525
526 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
528 let mut new = self.clone();
529 new.content = new_content;
530 new
531 }
532
533 pub(super) fn with_content_and_latest_edit(
538 &self,
539 new_content: TimelineItemContent,
540 edit_json: Option<Raw<AnySyncTimelineEvent>>,
541 ) -> Self {
542 let mut new = self.clone();
543 new.content = new_content;
544 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
545 r.latest_edit_json = edit_json;
546 }
547 new
548 }
549
550 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
552 Self { sender_profile, ..self.clone() }
553 }
554
555 pub(super) fn with_encryption_info(
557 &self,
558 encryption_info: Option<Arc<EncryptionInfo>>,
559 ) -> Self {
560 let mut new = self.clone();
561 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
562 r.encryption_info = encryption_info;
563 }
564
565 new
566 }
567
568 pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
570 let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
571 content: self.content.clone(),
572 original_json: self.original_json().cloned(),
573 latest_edit_json: self.latest_edit_json().cloned(),
574 });
575 let content = self.content.redact(rules);
576 let kind = match &self.kind {
577 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
578 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
579 };
580 Self {
581 sender: self.sender.clone(),
582 sender_profile: self.sender_profile.clone(),
583 forwarder: self.forwarder.clone(),
584 forwarder_profile: self.forwarder_profile.clone(),
585 timestamp: self.timestamp,
586 content,
587 unredacted_item,
588 redaction_send_state: None,
589 edit_send_state: None,
590 kind,
591 is_room_encrypted: self.is_room_encrypted,
592 }
593 }
594
595 pub(super) fn unredact(&self) -> Self {
599 let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
600 let kind = match &self.kind {
601 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
602 EventTimelineItemKind::Remote(r) => {
603 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
604 original_json: unredacted_item.original_json.clone(),
605 latest_edit_json: unredacted_item.latest_edit_json.clone(),
606 ..r.clone()
607 })
608 }
609 };
610 Self {
611 sender: self.sender.clone(),
612 sender_profile: self.sender_profile.clone(),
613 forwarder: self.forwarder.clone(),
614 forwarder_profile: self.forwarder_profile.clone(),
615 timestamp: self.timestamp,
616 content: unredacted_item.content.clone(),
617 unredacted_item: None,
618 redaction_send_state: None,
619 edit_send_state: None,
620 kind,
621 is_room_encrypted: self.is_room_encrypted,
622 }
623 }
624
625 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
626 match &self.kind {
627 EventTimelineItemKind::Local(local) => {
628 if let Some(event_id) = local.event_id() {
629 TimelineItemHandle::Remote(event_id)
630 } else {
631 TimelineItemHandle::Local(
632 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
634 )
635 }
636 }
637 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
638 }
639 }
640
641 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
643 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
644 }
645
646 pub fn contains_only_emojis(&self) -> bool {
669 let body = match self.content() {
670 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
671 MsgLikeKind::Message(message) => match &message.msgtype {
672 MessageType::Text(text) => Some(text.body.as_str()),
673 MessageType::Audio(audio) => audio.caption(),
674 MessageType::File(file) => file.caption(),
675 MessageType::Image(image) => image.caption(),
676 MessageType::Video(video) => video.caption(),
677 _ => None,
678 },
679 MsgLikeKind::Sticker(_)
680 | MsgLikeKind::Poll(_)
681 | MsgLikeKind::Redacted
682 | MsgLikeKind::UnableToDecrypt(_)
683 | MsgLikeKind::Other(_)
684 | MsgLikeKind::LiveLocation(_) => None,
685 },
686 TimelineItemContent::MembershipChange(_)
687 | TimelineItemContent::ProfileChange(_)
688 | TimelineItemContent::OtherState(_)
689 | TimelineItemContent::FailedToParseMessageLike { .. }
690 | TimelineItemContent::FailedToParseState { .. }
691 | TimelineItemContent::CallInvite
692 | TimelineItemContent::RtcNotification { .. } => None,
693 };
694
695 if let Some(body) = body {
696 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
698
699 if graphemes.len() > 5 {
704 return false;
705 }
706
707 graphemes.iter().all(|g| emojis::get(g).is_some())
708 } else {
709 false
710 }
711 }
712}
713
714impl From<LocalEventTimelineItem> for EventTimelineItemKind {
715 fn from(value: LocalEventTimelineItem) -> Self {
716 EventTimelineItemKind::Local(value)
717 }
718}
719
720impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
721 fn from(value: RemoteEventTimelineItem) -> Self {
722 EventTimelineItemKind::Remote(value)
723 }
724}
725
726#[derive(Clone, Debug, Default, PartialEq, Eq)]
728pub struct Profile {
729 pub display_name: Option<String>,
731
732 pub display_name_ambiguous: bool,
738
739 pub avatar_url: Option<OwnedMxcUri>,
741
742 #[cfg(feature = "unstable-msc4426")]
744 pub status: Option<StatusProfileField>,
745
746 #[cfg(feature = "unstable-msc4426")]
748 pub call: Option<CallProfileField>,
749}
750
751impl Profile {
752 pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
753 match room.get_member_no_sync(user_id).await {
754 Ok(Some(member)) => Some(Profile {
755 display_name: member.display_name().map(ToOwned::to_owned),
756 display_name_ambiguous: member.name_ambiguous(),
757 avatar_url: member.avatar_url().map(ToOwned::to_owned),
758 #[cfg(feature = "unstable-msc4426")]
759 status: member.status().cloned(),
760 #[cfg(feature = "unstable-msc4426")]
761 call: member.call().cloned(),
762 }),
763 Ok(None) if room.are_members_synced() => Some(Profile::default()),
764 Ok(None) => None,
765 Err(e) => {
766 error!(%user_id, "Failed to fetch room member information: {e}");
767 None
768 }
769 }
770 }
771}
772
773#[derive(Clone, Debug)]
777pub enum TimelineDetails<T> {
778 Unavailable,
781
782 Pending,
784
785 Ready(T),
787
788 Error(Arc<Error>),
790}
791
792impl<T> TimelineDetails<T> {
793 pub fn from_initial_value(value: Option<T>) -> Self {
799 match value {
800 Some(v) => Self::Ready(v),
801 None => Self::Unavailable,
802 }
803 }
804
805 pub fn is_unavailable(&self) -> bool {
806 matches!(self, Self::Unavailable)
807 }
808
809 pub fn is_ready(&self) -> bool {
810 matches!(self, Self::Ready(_))
811 }
812}
813
814#[derive(Clone, Copy, Debug)]
816#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
817pub enum EventItemOrigin {
818 Local,
820 Sync,
822 Pagination,
824 Cache,
826}
827
828#[derive(Clone, Debug)]
830pub struct ReactionInfo {
831 pub timestamp: MilliSecondsSinceUnixEpoch,
832 pub send_state: Option<EventSendState>,
835}
836
837#[derive(Debug, Clone, Default)]
842pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
843
844impl Deref for ReactionsByKeyBySender {
845 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
846
847 fn deref(&self) -> &Self::Target {
848 &self.0
849 }
850}
851
852impl DerefMut for ReactionsByKeyBySender {
853 fn deref_mut(&mut self) -> &mut Self::Target {
854 &mut self.0
855 }
856}
857
858impl ReactionsByKeyBySender {
859 pub(crate) fn remove_reaction(
865 &mut self,
866 sender: &UserId,
867 annotation: &str,
868 ) -> Option<ReactionInfo> {
869 if let Some(by_user) = self.0.get_mut(annotation)
870 && let Some(info) = by_user.swap_remove(sender)
871 {
872 if by_user.is_empty() {
874 self.0.swap_remove(annotation);
875 }
876 return Some(info);
877 }
878 None
879 }
880}
881
882#[derive(Clone, Copy, Debug, Eq, PartialEq)]
884pub enum TimelineEventShieldState {
885 Red {
888 code: TimelineEventShieldStateCode,
890 },
891 Grey {
894 code: TimelineEventShieldStateCode,
896 },
897 None,
899}
900
901impl From<ShieldState> for TimelineEventShieldState {
902 fn from(value: ShieldState) -> Self {
903 match value {
904 ShieldState::Red { code, message: _ } => {
905 TimelineEventShieldState::Red { code: code.into() }
906 }
907 ShieldState::Grey { code, message: _ } => {
908 TimelineEventShieldState::Grey { code: code.into() }
909 }
910 ShieldState::None => TimelineEventShieldState::None,
911 }
912 }
913}
914
915#[derive(Clone, Copy, Debug, Eq, PartialEq)]
917#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
918pub enum TimelineEventShieldStateCode {
919 AuthenticityNotGuaranteed,
921 UnknownDevice,
923 UnsignedDevice,
925 UnverifiedIdentity,
927 VerificationViolation,
929 MismatchedSender,
932 SentInClear,
934}
935
936impl From<ShieldStateCode> for TimelineEventShieldStateCode {
937 fn from(value: ShieldStateCode) -> Self {
938 use TimelineEventShieldStateCode::*;
939 match value {
940 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
941 ShieldStateCode::UnknownDevice => UnknownDevice,
942 ShieldStateCode::UnsignedDevice => UnsignedDevice,
943 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
944 ShieldStateCode::VerificationViolation => VerificationViolation,
945 ShieldStateCode::MismatchedSender => MismatchedSender,
946 }
947 }
948}
949
950#[cfg(test)]
951mod tests {
952 use std::time::Duration;
953
954 use ruma::{
955 MilliSecondsSinceUnixEpoch,
956 events::{
957 AnySyncTimelineEvent,
958 beacon_info::BeaconInfoEventContent,
959 room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
960 },
961 owned_event_id, owned_user_id,
962 serde::Raw,
963 uint,
964 };
965 use serde_json::json;
966
967 use super::{
968 EventSendState, EventTimelineItem, EventTimelineItemKind, LiveLocationState,
969 LocalEventTimelineItem, Message, MsgLikeContent, MsgLikeKind, RemoteEventOrigin,
970 RemoteEventTimelineItem, TimelineDetails, TimelineItemContent,
971 };
972
973 fn message_content() -> TimelineItemContent {
974 TimelineItemContent::MsgLike(MsgLikeContent {
975 kind: MsgLikeKind::Message(Message {
976 msgtype: MessageType::Text(TextMessageEventContent::plain("hello")),
977 edited: false,
978 mentions: None,
979 }),
980 reactions: Default::default(),
981 thread_root: None,
982 in_reply_to: None,
983 thread_summary: None,
984 })
985 }
986
987 fn live_location_content() -> TimelineItemContent {
988 TimelineItemContent::MsgLike(MsgLikeContent {
989 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(BeaconInfoEventContent::new(
990 None,
991 Duration::from_secs(300),
992 true,
993 Some(MilliSecondsSinceUnixEpoch(uint!(1))),
994 ))),
995 reactions: Default::default(),
996 thread_root: None,
997 in_reply_to: None,
998 thread_summary: None,
999 })
1000 }
1001
1002 fn remote_item(
1003 content: TimelineItemContent,
1004 original_json: Option<Raw<AnySyncTimelineEvent>>,
1005 ) -> EventTimelineItem {
1006 EventTimelineItem::new(
1007 owned_user_id!("@alice:example.org"),
1008 TimelineDetails::Unavailable,
1009 None,
1010 None,
1011 MilliSecondsSinceUnixEpoch(uint!(1)),
1012 content,
1013 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
1014 event_id: owned_event_id!("$event"),
1015 transaction_id: None,
1016 read_receipts: Default::default(),
1017 is_own: false,
1018 is_highlighted: false,
1019 encryption_info: None,
1020 original_json,
1021 latest_edit_json: None,
1022 origin: RemoteEventOrigin::Sync,
1023 }),
1024 false,
1025 )
1026 }
1027
1028 fn local_unsent_item(content: TimelineItemContent) -> EventTimelineItem {
1029 EventTimelineItem::new(
1030 owned_user_id!("@alice:example.org"),
1031 TimelineDetails::Unavailable,
1032 None,
1033 None,
1034 MilliSecondsSinceUnixEpoch(uint!(1)),
1035 content,
1036 EventTimelineItemKind::Local(LocalEventTimelineItem {
1037 send_state: EventSendState::NotSentYet { progress: None },
1038 transaction_id: "t0".into(),
1039 send_handle: None,
1040 }),
1041 false,
1042 )
1043 }
1044
1045 fn sample_raw_event() -> Raw<AnySyncTimelineEvent> {
1046 Raw::from_json_string(
1047 json!({
1048 "content": RoomMessageEventContent::text_plain("hi"),
1049 "type": "m.room.message",
1050 "event_id": "$event",
1051 "room_id": "!room:example.org",
1052 "origin_server_ts": 1,
1053 "sender": "@alice:example.org",
1054 })
1055 .to_string(),
1056 )
1057 .unwrap()
1058 }
1059
1060 #[test]
1061 fn cannot_reply_to_local_unsent_events() {
1062 let item = local_unsent_item(message_content());
1063 assert!(!item.can_be_replied_to());
1064 }
1065
1066 #[test]
1067 fn can_reply_to_messages() {
1068 let item = remote_item(message_content(), None);
1069 assert!(item.can_be_replied_to());
1070 }
1071
1072 #[test]
1073 fn cannot_reply_to_live_location_events() {
1074 let item = remote_item(live_location_content(), Some(sample_raw_event()));
1075 assert!(!item.can_be_replied_to());
1076 }
1077
1078 #[test]
1079 fn cannot_reply_to_non_messages_with_no_json() {
1080 let item = remote_item(TimelineItemContent::CallInvite, None);
1081 assert!(!item.can_be_replied_to());
1082 }
1083
1084 #[test]
1085 fn can_reply_to_non_messages_with_json() {
1086 let item = remote_item(TimelineItemContent::CallInvite, Some(sample_raw_event()));
1087 assert!(item.can_be_replied_to());
1088 }
1089}