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,
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 unicode_segmentation::UnicodeSegmentation;
36
37mod content;
38mod local;
39mod remote;
40
41pub use self::{
42 content::{
43 AnyOtherStateEventContentChange, BeaconInfo, EmbeddedEvent, EncryptedMessage,
44 InReplyToDetails, LiveLocationState, MemberProfileChange, MembershipChange, Message,
45 MsgLikeContent, MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState,
46 RoomMembershipChange, RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
47 },
48 local::{EventSendState, MediaUploadProgress},
49};
50pub(super) use self::{
51 content::{
52 beacon_info_matches, extract_bundled_edit_event_json, extract_poll_edit_content,
53 extract_room_msg_edit_content,
54 },
55 local::LocalEventTimelineItem,
56 remote::{RemoteEventOrigin, RemoteEventTimelineItem},
57};
58
59#[derive(Clone, Debug)]
65pub struct EventTimelineItem {
66 pub(super) sender: OwnedUserId,
68 pub(super) sender_profile: TimelineDetails<Profile>,
70 pub(super) forwarder: Option<OwnedUserId>,
75 pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
80 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
82 pub(super) content: TimelineItemContent,
84 pub(super) kind: EventTimelineItemKind,
86 pub(super) is_room_encrypted: bool,
90}
91
92#[derive(Clone, Debug)]
93pub(super) enum EventTimelineItemKind {
94 Local(LocalEventTimelineItem),
96 Remote(RemoteEventTimelineItem),
98}
99
100#[derive(Clone, Debug, Eq, Hash, PartialEq)]
102pub enum TimelineEventItemId {
103 TransactionId(OwnedTransactionId),
106 EventId(OwnedEventId),
108}
109
110pub(crate) enum TimelineItemHandle<'a> {
116 Remote(&'a EventId),
117 Local(&'a SendHandle),
118}
119
120impl EventTimelineItem {
121 #[allow(clippy::too_many_arguments)]
122 pub(super) fn new(
123 sender: OwnedUserId,
124 sender_profile: TimelineDetails<Profile>,
125 forwarder: Option<OwnedUserId>,
126 forwarder_profile: Option<TimelineDetails<Profile>>,
127 timestamp: MilliSecondsSinceUnixEpoch,
128 content: TimelineItemContent,
129 kind: EventTimelineItemKind,
130 is_room_encrypted: bool,
131 ) -> Self {
132 Self {
133 sender,
134 sender_profile,
135 forwarder,
136 forwarder_profile,
137 timestamp,
138 content,
139 kind,
140 is_room_encrypted,
141 }
142 }
143
144 pub fn is_local_echo(&self) -> bool {
151 matches!(self.kind, EventTimelineItemKind::Local(_))
152 }
153
154 pub fn is_remote_event(&self) -> bool {
162 matches!(self.kind, EventTimelineItemKind::Remote(_))
163 }
164
165 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
167 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
168 }
169
170 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
172 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
173 }
174
175 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
178 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
179 }
180
181 pub fn send_state(&self) -> Option<&EventSendState> {
183 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
184 }
185
186 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
188 match &self.kind {
189 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
190 EventTimelineItemKind::Remote(_) => None,
191 }
192 }
193
194 pub fn identifier(&self) -> TimelineEventItemId {
200 match &self.kind {
201 EventTimelineItemKind::Local(local) => local.identifier(),
202 EventTimelineItemKind::Remote(remote) => {
203 TimelineEventItemId::EventId(remote.event_id.clone())
204 }
205 }
206 }
207
208 pub fn transaction_id(&self) -> Option<&TransactionId> {
213 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
214 }
215
216 pub fn event_id(&self) -> Option<&EventId> {
225 match &self.kind {
226 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
227 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
228 }
229 }
230
231 pub fn sender(&self) -> &UserId {
233 &self.sender
234 }
235
236 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
238 &self.sender_profile
239 }
240
241 pub fn forwarder(&self) -> Option<&UserId> {
246 self.forwarder.as_deref()
247 }
248
249 pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
254 self.forwarder_profile.as_ref()
255 }
256
257 pub fn content(&self) -> &TimelineItemContent {
259 &self.content
260 }
261
262 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
264 &mut self.content
265 }
266
267 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
274 static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
275 LazyLock::new(Default::default);
276 match &self.kind {
277 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
278 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
279 }
280 }
281
282 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
288 self.timestamp
289 }
290
291 pub fn is_own(&self) -> bool {
293 match &self.kind {
294 EventTimelineItemKind::Local(_) => true,
295 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
296 }
297 }
298
299 pub fn is_editable(&self) -> bool {
301 if !self.is_own() {
305 return false;
307 }
308
309 match self.content() {
310 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
311 MsgLikeKind::Message(message) => match message.msgtype() {
312 MessageType::Text(_)
313 | MessageType::Emote(_)
314 | MessageType::Audio(_)
315 | MessageType::File(_)
316 | MessageType::Image(_)
317 | MessageType::Video(_) => true,
318 #[cfg(feature = "unstable-msc4274")]
319 MessageType::Gallery(_) => true,
320 _ => false,
321 },
322 MsgLikeKind::Poll(poll) => {
323 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
324 }
325 _ => false,
327 },
328 _ => {
329 false
331 }
332 }
333 }
334
335 pub fn is_highlighted(&self) -> bool {
337 match &self.kind {
338 EventTimelineItemKind::Local(_) => false,
339 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
340 }
341 }
342
343 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
345 match &self.kind {
346 EventTimelineItemKind::Local(_) => None,
347 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
348 }
349 }
350
351 pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
354 if !self.is_room_encrypted || self.is_local_echo() {
355 return TimelineEventShieldState::None;
356 }
357
358 if self.content().is_unable_to_decrypt() {
360 return TimelineEventShieldState::None;
361 }
362
363 if let Some(live_location) = self.content().as_live_location_state() {
374 return match live_location.latest_location() {
375 None => TimelineEventShieldState::None,
376 Some(beacon) => match beacon.encryption_info() {
377 Some(info) => {
378 if strict {
379 info.verification_state.to_shield_state_strict().into()
380 } else {
381 info.verification_state.to_shield_state_lax().into()
382 }
383 }
384 None => TimelineEventShieldState::Red {
385 code: TimelineEventShieldStateCode::SentInClear,
386 },
387 },
388 };
389 }
390
391 match self.encryption_info() {
392 Some(info) => {
393 if strict {
394 info.verification_state.to_shield_state_strict().into()
395 } else {
396 info.verification_state.to_shield_state_lax().into()
397 }
398 }
399 None => {
400 TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
401 }
402 }
403 }
404
405 pub fn can_be_replied_to(&self) -> bool {
407 if self.event_id().is_none() {
409 false
410 } else if self.content.is_message() {
411 true
412 } else {
413 self.latest_json().is_some()
414 }
415 }
416
417 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
423 match &self.kind {
424 EventTimelineItemKind::Local(_) => None,
425 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
426 }
427 }
428
429 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
431 match &self.kind {
432 EventTimelineItemKind::Local(_) => None,
433 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
434 }
435 }
436
437 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
440 self.latest_edit_json().or_else(|| self.original_json())
441 }
442
443 pub fn origin(&self) -> Option<EventItemOrigin> {
447 match &self.kind {
448 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
449 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
450 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
451 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
452 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
453 RemoteEventOrigin::Unknown => None,
454 },
455 }
456 }
457
458 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
459 self.content = content;
460 }
461
462 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
464 Self { kind: kind.into(), ..self.clone() }
465 }
466
467 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
469 let mut new = self.clone();
470 new.content = new_content;
471 new
472 }
473
474 pub(super) fn with_content_and_latest_edit(
479 &self,
480 new_content: TimelineItemContent,
481 edit_json: Option<Raw<AnySyncTimelineEvent>>,
482 ) -> Self {
483 let mut new = self.clone();
484 new.content = new_content;
485 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
486 r.latest_edit_json = edit_json;
487 }
488 new
489 }
490
491 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
493 Self { sender_profile, ..self.clone() }
494 }
495
496 pub(super) fn with_encryption_info(
498 &self,
499 encryption_info: Option<Arc<EncryptionInfo>>,
500 ) -> Self {
501 let mut new = self.clone();
502 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
503 r.encryption_info = encryption_info;
504 }
505
506 new
507 }
508
509 pub(super) fn redact(&self, rules: &RedactionRules) -> Self {
511 let content = self.content.redact(rules);
512 let kind = match &self.kind {
513 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
514 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
515 };
516 Self {
517 sender: self.sender.clone(),
518 sender_profile: self.sender_profile.clone(),
519 forwarder: self.forwarder.clone(),
520 forwarder_profile: self.forwarder_profile.clone(),
521 timestamp: self.timestamp,
522 content,
523 kind,
524 is_room_encrypted: self.is_room_encrypted,
525 }
526 }
527
528 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
529 match &self.kind {
530 EventTimelineItemKind::Local(local) => {
531 if let Some(event_id) = local.event_id() {
532 TimelineItemHandle::Remote(event_id)
533 } else {
534 TimelineItemHandle::Local(
535 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
537 )
538 }
539 }
540 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
541 }
542 }
543
544 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
546 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
547 }
548
549 pub fn contains_only_emojis(&self) -> bool {
572 let body = match self.content() {
573 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
574 MsgLikeKind::Message(message) => match &message.msgtype {
575 MessageType::Text(text) => Some(text.body.as_str()),
576 MessageType::Audio(audio) => audio.caption(),
577 MessageType::File(file) => file.caption(),
578 MessageType::Image(image) => image.caption(),
579 MessageType::Video(video) => video.caption(),
580 _ => None,
581 },
582 MsgLikeKind::Sticker(_)
583 | MsgLikeKind::Poll(_)
584 | MsgLikeKind::Redacted
585 | MsgLikeKind::UnableToDecrypt(_)
586 | MsgLikeKind::Other(_)
587 | MsgLikeKind::LiveLocation(_) => None,
588 },
589 TimelineItemContent::MembershipChange(_)
590 | TimelineItemContent::ProfileChange(_)
591 | TimelineItemContent::OtherState(_)
592 | TimelineItemContent::FailedToParseMessageLike { .. }
593 | TimelineItemContent::FailedToParseState { .. }
594 | TimelineItemContent::CallInvite
595 | TimelineItemContent::RtcNotification => None,
596 };
597
598 if let Some(body) = body {
599 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
601
602 if graphemes.len() > 5 {
607 return false;
608 }
609
610 graphemes.iter().all(|g| emojis::get(g).is_some())
611 } else {
612 false
613 }
614 }
615}
616
617impl From<LocalEventTimelineItem> for EventTimelineItemKind {
618 fn from(value: LocalEventTimelineItem) -> Self {
619 EventTimelineItemKind::Local(value)
620 }
621}
622
623impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
624 fn from(value: RemoteEventTimelineItem) -> Self {
625 EventTimelineItemKind::Remote(value)
626 }
627}
628
629#[derive(Clone, Debug, Default, PartialEq, Eq)]
631pub struct Profile {
632 pub display_name: Option<String>,
634
635 pub display_name_ambiguous: bool,
641
642 pub avatar_url: Option<OwnedMxcUri>,
644}
645
646#[derive(Clone, Debug)]
650pub enum TimelineDetails<T> {
651 Unavailable,
654
655 Pending,
657
658 Ready(T),
660
661 Error(Arc<Error>),
663}
664
665impl<T> TimelineDetails<T> {
666 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
667 match value {
668 Some(v) => Self::Ready(v),
669 None => Self::Unavailable,
670 }
671 }
672
673 pub fn is_unavailable(&self) -> bool {
674 matches!(self, Self::Unavailable)
675 }
676
677 pub fn is_ready(&self) -> bool {
678 matches!(self, Self::Ready(_))
679 }
680}
681
682#[derive(Clone, Copy, Debug)]
684#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
685pub enum EventItemOrigin {
686 Local,
688 Sync,
690 Pagination,
692 Cache,
694}
695
696#[derive(Clone, Debug)]
698pub enum ReactionStatus {
699 LocalToLocal(Option<SendReactionHandle>),
703 LocalToRemote(Option<SendHandle>),
707 RemoteToRemote(OwnedEventId),
711}
712
713#[derive(Clone, Debug)]
715pub struct ReactionInfo {
716 pub timestamp: MilliSecondsSinceUnixEpoch,
717 pub status: ReactionStatus,
719}
720
721#[derive(Debug, Clone, Default)]
726pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
727
728impl Deref for ReactionsByKeyBySender {
729 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
730
731 fn deref(&self) -> &Self::Target {
732 &self.0
733 }
734}
735
736impl DerefMut for ReactionsByKeyBySender {
737 fn deref_mut(&mut self) -> &mut Self::Target {
738 &mut self.0
739 }
740}
741
742impl ReactionsByKeyBySender {
743 pub(crate) fn remove_reaction(
749 &mut self,
750 sender: &UserId,
751 annotation: &str,
752 ) -> Option<ReactionInfo> {
753 if let Some(by_user) = self.0.get_mut(annotation)
754 && let Some(info) = by_user.swap_remove(sender)
755 {
756 if by_user.is_empty() {
758 self.0.swap_remove(annotation);
759 }
760 return Some(info);
761 }
762 None
763 }
764}
765
766#[derive(Clone, Copy, Debug, Eq, PartialEq)]
768pub enum TimelineEventShieldState {
769 Red {
772 code: TimelineEventShieldStateCode,
774 },
775 Grey {
778 code: TimelineEventShieldStateCode,
780 },
781 None,
783}
784
785impl From<ShieldState> for TimelineEventShieldState {
786 fn from(value: ShieldState) -> Self {
787 match value {
788 ShieldState::Red { code, message: _ } => {
789 TimelineEventShieldState::Red { code: code.into() }
790 }
791 ShieldState::Grey { code, message: _ } => {
792 TimelineEventShieldState::Grey { code: code.into() }
793 }
794 ShieldState::None => TimelineEventShieldState::None,
795 }
796 }
797}
798
799#[derive(Clone, Copy, Debug, Eq, PartialEq)]
801#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
802pub enum TimelineEventShieldStateCode {
803 AuthenticityNotGuaranteed,
805 UnknownDevice,
807 UnsignedDevice,
809 UnverifiedIdentity,
811 VerificationViolation,
813 MismatchedSender,
816 SentInClear,
818}
819
820impl From<ShieldStateCode> for TimelineEventShieldStateCode {
821 fn from(value: ShieldStateCode) -> Self {
822 use TimelineEventShieldStateCode::*;
823 match value {
824 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
825 ShieldStateCode::UnknownDevice => UnknownDevice,
826 ShieldStateCode::UnsignedDevice => UnsignedDevice,
827 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
828 ShieldStateCode::VerificationViolation => VerificationViolation,
829 ShieldStateCode::MismatchedSender => MismatchedSender,
830 }
831 }
832}