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 {
435 self.latest_json().is_some()
436 }
437 }
438
439 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
445 match &self.kind {
446 EventTimelineItemKind::Local(_) => None,
447 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
448 }
449 }
450
451 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
453 match &self.kind {
454 EventTimelineItemKind::Local(_) => None,
455 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
456 }
457 }
458
459 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
462 self.latest_edit_json().or_else(|| self.original_json())
463 }
464
465 pub fn origin(&self) -> Option<EventItemOrigin> {
469 match &self.kind {
470 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
471 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
472 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
473 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
474 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
475 RemoteEventOrigin::Unknown => None,
476 },
477 }
478 }
479
480 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
481 self.content = content;
482 }
483
484 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
486 Self { kind: kind.into(), ..self.clone() }
487 }
488
489 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
491 let mut new = self.clone();
492 new.content = new_content;
493 new
494 }
495
496 pub(super) fn with_content_and_latest_edit(
501 &self,
502 new_content: TimelineItemContent,
503 edit_json: Option<Raw<AnySyncTimelineEvent>>,
504 ) -> Self {
505 let mut new = self.clone();
506 new.content = new_content;
507 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
508 r.latest_edit_json = edit_json;
509 }
510 new
511 }
512
513 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
515 Self { sender_profile, ..self.clone() }
516 }
517
518 pub(super) fn with_encryption_info(
520 &self,
521 encryption_info: Option<Arc<EncryptionInfo>>,
522 ) -> Self {
523 let mut new = self.clone();
524 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
525 r.encryption_info = encryption_info;
526 }
527
528 new
529 }
530
531 pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
533 let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
534 content: self.content.clone(),
535 original_json: self.original_json().cloned(),
536 latest_edit_json: self.latest_edit_json().cloned(),
537 });
538 let content = self.content.redact(rules);
539 let kind = match &self.kind {
540 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
541 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
542 };
543 Self {
544 sender: self.sender.clone(),
545 sender_profile: self.sender_profile.clone(),
546 forwarder: self.forwarder.clone(),
547 forwarder_profile: self.forwarder_profile.clone(),
548 timestamp: self.timestamp,
549 content,
550 unredacted_item,
551 kind,
552 is_room_encrypted: self.is_room_encrypted,
553 }
554 }
555
556 pub(super) fn unredact(&self) -> Self {
560 let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
561 let kind = match &self.kind {
562 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
563 EventTimelineItemKind::Remote(r) => {
564 EventTimelineItemKind::Remote(RemoteEventTimelineItem {
565 original_json: unredacted_item.original_json.clone(),
566 latest_edit_json: unredacted_item.latest_edit_json.clone(),
567 ..r.clone()
568 })
569 }
570 };
571 Self {
572 sender: self.sender.clone(),
573 sender_profile: self.sender_profile.clone(),
574 forwarder: self.forwarder.clone(),
575 forwarder_profile: self.forwarder_profile.clone(),
576 timestamp: self.timestamp,
577 content: unredacted_item.content.clone(),
578 unredacted_item: None,
579 kind,
580 is_room_encrypted: self.is_room_encrypted,
581 }
582 }
583
584 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
585 match &self.kind {
586 EventTimelineItemKind::Local(local) => {
587 if let Some(event_id) = local.event_id() {
588 TimelineItemHandle::Remote(event_id)
589 } else {
590 TimelineItemHandle::Local(
591 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
593 )
594 }
595 }
596 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
597 }
598 }
599
600 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
602 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
603 }
604
605 pub fn contains_only_emojis(&self) -> bool {
628 let body = match self.content() {
629 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
630 MsgLikeKind::Message(message) => match &message.msgtype {
631 MessageType::Text(text) => Some(text.body.as_str()),
632 MessageType::Audio(audio) => audio.caption(),
633 MessageType::File(file) => file.caption(),
634 MessageType::Image(image) => image.caption(),
635 MessageType::Video(video) => video.caption(),
636 _ => None,
637 },
638 MsgLikeKind::Sticker(_)
639 | MsgLikeKind::Poll(_)
640 | MsgLikeKind::Redacted
641 | MsgLikeKind::UnableToDecrypt(_)
642 | MsgLikeKind::Other(_)
643 | MsgLikeKind::LiveLocation(_) => None,
644 },
645 TimelineItemContent::MembershipChange(_)
646 | TimelineItemContent::ProfileChange(_)
647 | TimelineItemContent::OtherState(_)
648 | TimelineItemContent::FailedToParseMessageLike { .. }
649 | TimelineItemContent::FailedToParseState { .. }
650 | TimelineItemContent::CallInvite
651 | TimelineItemContent::RtcNotification { .. } => None,
652 };
653
654 if let Some(body) = body {
655 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
657
658 if graphemes.len() > 5 {
663 return false;
664 }
665
666 graphemes.iter().all(|g| emojis::get(g).is_some())
667 } else {
668 false
669 }
670 }
671}
672
673impl From<LocalEventTimelineItem> for EventTimelineItemKind {
674 fn from(value: LocalEventTimelineItem) -> Self {
675 EventTimelineItemKind::Local(value)
676 }
677}
678
679impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
680 fn from(value: RemoteEventTimelineItem) -> Self {
681 EventTimelineItemKind::Remote(value)
682 }
683}
684
685#[derive(Clone, Debug, Default, PartialEq, Eq)]
687pub struct Profile {
688 pub display_name: Option<String>,
690
691 pub display_name_ambiguous: bool,
697
698 pub avatar_url: Option<OwnedMxcUri>,
700}
701
702impl Profile {
703 pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
704 match room.get_member_no_sync(user_id).await {
705 Ok(Some(member)) => Some(Profile {
706 display_name: member.display_name().map(ToOwned::to_owned),
707 display_name_ambiguous: member.name_ambiguous(),
708 avatar_url: member.avatar_url().map(ToOwned::to_owned),
709 }),
710 Ok(None) if room.are_members_synced() => Some(Profile::default()),
711 Ok(None) => None,
712 Err(e) => {
713 error!(%user_id, "Failed to fetch room member information: {e}");
714 None
715 }
716 }
717 }
718}
719
720#[derive(Clone, Debug)]
724pub enum TimelineDetails<T> {
725 Unavailable,
728
729 Pending,
731
732 Ready(T),
734
735 Error(Arc<Error>),
737}
738
739impl<T> TimelineDetails<T> {
740 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
741 match value {
742 Some(v) => Self::Ready(v),
743 None => Self::Unavailable,
744 }
745 }
746
747 pub fn is_unavailable(&self) -> bool {
748 matches!(self, Self::Unavailable)
749 }
750
751 pub fn is_ready(&self) -> bool {
752 matches!(self, Self::Ready(_))
753 }
754}
755
756#[derive(Clone, Copy, Debug)]
758#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
759pub enum EventItemOrigin {
760 Local,
762 Sync,
764 Pagination,
766 Cache,
768}
769
770#[derive(Clone, Debug)]
772pub enum ReactionStatus {
773 LocalToLocal(Option<SendReactionHandle>),
777 LocalToRemote(Option<SendHandle>),
781 RemoteToRemote(OwnedEventId),
785}
786
787#[derive(Clone, Debug)]
789pub struct ReactionInfo {
790 pub timestamp: MilliSecondsSinceUnixEpoch,
791 pub status: ReactionStatus,
793}
794
795#[derive(Debug, Clone, Default)]
800pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
801
802impl Deref for ReactionsByKeyBySender {
803 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
804
805 fn deref(&self) -> &Self::Target {
806 &self.0
807 }
808}
809
810impl DerefMut for ReactionsByKeyBySender {
811 fn deref_mut(&mut self) -> &mut Self::Target {
812 &mut self.0
813 }
814}
815
816impl ReactionsByKeyBySender {
817 pub(crate) fn remove_reaction(
823 &mut self,
824 sender: &UserId,
825 annotation: &str,
826 ) -> Option<ReactionInfo> {
827 if let Some(by_user) = self.0.get_mut(annotation)
828 && let Some(info) = by_user.swap_remove(sender)
829 {
830 if by_user.is_empty() {
832 self.0.swap_remove(annotation);
833 }
834 return Some(info);
835 }
836 None
837 }
838}
839
840#[derive(Clone, Copy, Debug, Eq, PartialEq)]
842pub enum TimelineEventShieldState {
843 Red {
846 code: TimelineEventShieldStateCode,
848 },
849 Grey {
852 code: TimelineEventShieldStateCode,
854 },
855 None,
857}
858
859impl From<ShieldState> for TimelineEventShieldState {
860 fn from(value: ShieldState) -> Self {
861 match value {
862 ShieldState::Red { code, message: _ } => {
863 TimelineEventShieldState::Red { code: code.into() }
864 }
865 ShieldState::Grey { code, message: _ } => {
866 TimelineEventShieldState::Grey { code: code.into() }
867 }
868 ShieldState::None => TimelineEventShieldState::None,
869 }
870 }
871}
872
873#[derive(Clone, Copy, Debug, Eq, PartialEq)]
875#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
876pub enum TimelineEventShieldStateCode {
877 AuthenticityNotGuaranteed,
879 UnknownDevice,
881 UnsignedDevice,
883 UnverifiedIdentity,
885 VerificationViolation,
887 MismatchedSender,
890 SentInClear,
892}
893
894impl From<ShieldStateCode> for TimelineEventShieldStateCode {
895 fn from(value: ShieldStateCode) -> Self {
896 use TimelineEventShieldStateCode::*;
897 match value {
898 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
899 ShieldStateCode::UnknownDevice => UnknownDevice,
900 ShieldStateCode::UnsignedDevice => UnsignedDevice,
901 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
902 ShieldStateCode::VerificationViolation => VerificationViolation,
903 ShieldStateCode::MismatchedSender => MismatchedSender,
904 }
905 }
906}