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 AnyOtherFullStateEventContent, EmbeddedEvent, EncryptedMessage, InReplyToDetails,
44 MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind,
45 OtherMessageLike, OtherState, PollResult, PollState, RoomMembershipChange,
46 RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
47 },
48 local::{EventSendState, MediaUploadProgress},
49};
50pub(super) use self::{
51 content::{
52 extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
53 },
54 local::LocalEventTimelineItem,
55 remote::{RemoteEventOrigin, RemoteEventTimelineItem},
56};
57
58#[derive(Clone, Debug)]
64pub struct EventTimelineItem {
65 pub(super) sender: OwnedUserId,
67 pub(super) sender_profile: TimelineDetails<Profile>,
69 pub(super) forwarder: Option<OwnedUserId>,
74 pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
79 pub(super) timestamp: MilliSecondsSinceUnixEpoch,
81 pub(super) content: TimelineItemContent,
83 pub(super) kind: EventTimelineItemKind,
85 pub(super) is_room_encrypted: bool,
89}
90
91#[derive(Clone, Debug)]
92pub(super) enum EventTimelineItemKind {
93 Local(LocalEventTimelineItem),
95 Remote(RemoteEventTimelineItem),
97}
98
99#[derive(Clone, Debug, Eq, Hash, PartialEq)]
101pub enum TimelineEventItemId {
102 TransactionId(OwnedTransactionId),
105 EventId(OwnedEventId),
107}
108
109pub(crate) enum TimelineItemHandle<'a> {
115 Remote(&'a EventId),
116 Local(&'a SendHandle),
117}
118
119impl EventTimelineItem {
120 #[allow(clippy::too_many_arguments)]
121 pub(super) fn new(
122 sender: OwnedUserId,
123 sender_profile: TimelineDetails<Profile>,
124 forwarder: Option<OwnedUserId>,
125 forwarder_profile: Option<TimelineDetails<Profile>>,
126 timestamp: MilliSecondsSinceUnixEpoch,
127 content: TimelineItemContent,
128 kind: EventTimelineItemKind,
129 is_room_encrypted: bool,
130 ) -> Self {
131 Self {
132 sender,
133 sender_profile,
134 forwarder,
135 forwarder_profile,
136 timestamp,
137 content,
138 kind,
139 is_room_encrypted,
140 }
141 }
142
143 pub fn is_local_echo(&self) -> bool {
150 matches!(self.kind, EventTimelineItemKind::Local(_))
151 }
152
153 pub fn is_remote_event(&self) -> bool {
161 matches!(self.kind, EventTimelineItemKind::Remote(_))
162 }
163
164 pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
166 as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
167 }
168
169 pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
171 as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
172 }
173
174 pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
177 as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
178 }
179
180 pub fn send_state(&self) -> Option<&EventSendState> {
182 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
183 }
184
185 pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
187 match &self.kind {
188 EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
189 EventTimelineItemKind::Remote(_) => None,
190 }
191 }
192
193 pub fn identifier(&self) -> TimelineEventItemId {
199 match &self.kind {
200 EventTimelineItemKind::Local(local) => local.identifier(),
201 EventTimelineItemKind::Remote(remote) => {
202 TimelineEventItemId::EventId(remote.event_id.clone())
203 }
204 }
205 }
206
207 pub fn transaction_id(&self) -> Option<&TransactionId> {
212 as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
213 }
214
215 pub fn event_id(&self) -> Option<&EventId> {
224 match &self.kind {
225 EventTimelineItemKind::Local(local_event) => local_event.event_id(),
226 EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
227 }
228 }
229
230 pub fn sender(&self) -> &UserId {
232 &self.sender
233 }
234
235 pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
237 &self.sender_profile
238 }
239
240 pub fn forwarder(&self) -> Option<&UserId> {
245 self.forwarder.as_deref()
246 }
247
248 pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
253 self.forwarder_profile.as_ref()
254 }
255
256 pub fn content(&self) -> &TimelineItemContent {
258 &self.content
259 }
260
261 pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
263 &mut self.content
264 }
265
266 pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
273 static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
274 LazyLock::new(Default::default);
275 match &self.kind {
276 EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
277 EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
278 }
279 }
280
281 pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
287 self.timestamp
288 }
289
290 pub fn is_own(&self) -> bool {
292 match &self.kind {
293 EventTimelineItemKind::Local(_) => true,
294 EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
295 }
296 }
297
298 pub fn is_editable(&self) -> bool {
300 if !self.is_own() {
304 return false;
306 }
307
308 match self.content() {
309 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
310 MsgLikeKind::Message(message) => match message.msgtype() {
311 MessageType::Text(_)
312 | MessageType::Emote(_)
313 | MessageType::Audio(_)
314 | MessageType::File(_)
315 | MessageType::Image(_)
316 | MessageType::Video(_) => true,
317 #[cfg(feature = "unstable-msc4274")]
318 MessageType::Gallery(_) => true,
319 _ => false,
320 },
321 MsgLikeKind::Poll(poll) => {
322 poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
323 }
324 _ => false,
326 },
327 _ => {
328 false
330 }
331 }
332 }
333
334 pub fn is_highlighted(&self) -> bool {
336 match &self.kind {
337 EventTimelineItemKind::Local(_) => false,
338 EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
339 }
340 }
341
342 pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
344 match &self.kind {
345 EventTimelineItemKind::Local(_) => None,
346 EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
347 }
348 }
349
350 pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
353 if !self.is_room_encrypted || self.is_local_echo() {
354 return TimelineEventShieldState::None;
355 }
356
357 if self.content().is_unable_to_decrypt() {
359 return TimelineEventShieldState::None;
360 }
361
362 match self.encryption_info() {
363 Some(info) => {
364 if strict {
365 info.verification_state.to_shield_state_strict().into()
366 } else {
367 info.verification_state.to_shield_state_lax().into()
368 }
369 }
370 None => {
371 TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
372 }
373 }
374 }
375
376 pub fn can_be_replied_to(&self) -> bool {
378 if self.event_id().is_none() {
380 false
381 } else if self.content.is_message() {
382 true
383 } else {
384 self.latest_json().is_some()
385 }
386 }
387
388 pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
394 match &self.kind {
395 EventTimelineItemKind::Local(_) => None,
396 EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
397 }
398 }
399
400 pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
402 match &self.kind {
403 EventTimelineItemKind::Local(_) => None,
404 EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
405 }
406 }
407
408 pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
411 self.latest_edit_json().or_else(|| self.original_json())
412 }
413
414 pub fn origin(&self) -> Option<EventItemOrigin> {
418 match &self.kind {
419 EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
420 EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
421 RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
422 RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
423 RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
424 RemoteEventOrigin::Unknown => None,
425 },
426 }
427 }
428
429 pub(super) fn set_content(&mut self, content: TimelineItemContent) {
430 self.content = content;
431 }
432
433 pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
435 Self { kind: kind.into(), ..self.clone() }
436 }
437
438 pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
440 let mut new = self.clone();
441 new.content = new_content;
442 new
443 }
444
445 pub(super) fn with_content_and_latest_edit(
450 &self,
451 new_content: TimelineItemContent,
452 edit_json: Option<Raw<AnySyncTimelineEvent>>,
453 ) -> Self {
454 let mut new = self.clone();
455 new.content = new_content;
456 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
457 r.latest_edit_json = edit_json;
458 }
459 new
460 }
461
462 pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
464 Self { sender_profile, ..self.clone() }
465 }
466
467 pub(super) fn with_encryption_info(
469 &self,
470 encryption_info: Option<Arc<EncryptionInfo>>,
471 ) -> Self {
472 let mut new = self.clone();
473 if let EventTimelineItemKind::Remote(r) = &mut new.kind {
474 r.encryption_info = encryption_info;
475 }
476
477 new
478 }
479
480 pub(super) fn redact(&self, rules: &RedactionRules) -> Self {
482 let content = self.content.redact(rules);
483 let kind = match &self.kind {
484 EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
485 EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
486 };
487 Self {
488 sender: self.sender.clone(),
489 sender_profile: self.sender_profile.clone(),
490 forwarder: self.forwarder.clone(),
491 forwarder_profile: self.forwarder_profile.clone(),
492 timestamp: self.timestamp,
493 content,
494 kind,
495 is_room_encrypted: self.is_room_encrypted,
496 }
497 }
498
499 pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
500 match &self.kind {
501 EventTimelineItemKind::Local(local) => {
502 if let Some(event_id) = local.event_id() {
503 TimelineItemHandle::Remote(event_id)
504 } else {
505 TimelineItemHandle::Local(
506 local.send_handle.as_ref().expect("Unexpected missing send_handle"),
508 )
509 }
510 }
511 EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
512 }
513 }
514
515 pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
517 as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
518 }
519
520 pub fn contains_only_emojis(&self) -> bool {
543 let body = match self.content() {
544 TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
545 MsgLikeKind::Message(message) => match &message.msgtype {
546 MessageType::Text(text) => Some(text.body.as_str()),
547 MessageType::Audio(audio) => audio.caption(),
548 MessageType::File(file) => file.caption(),
549 MessageType::Image(image) => image.caption(),
550 MessageType::Video(video) => video.caption(),
551 _ => None,
552 },
553 MsgLikeKind::Sticker(_)
554 | MsgLikeKind::Poll(_)
555 | MsgLikeKind::Redacted
556 | MsgLikeKind::UnableToDecrypt(_)
557 | MsgLikeKind::Other(_) => None,
558 },
559 TimelineItemContent::MembershipChange(_)
560 | TimelineItemContent::ProfileChange(_)
561 | TimelineItemContent::OtherState(_)
562 | TimelineItemContent::FailedToParseMessageLike { .. }
563 | TimelineItemContent::FailedToParseState { .. }
564 | TimelineItemContent::CallInvite
565 | TimelineItemContent::RtcNotification => None,
566 };
567
568 if let Some(body) = body {
569 let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
571
572 if graphemes.len() > 5 {
577 return false;
578 }
579
580 graphemes.iter().all(|g| emojis::get(g).is_some())
581 } else {
582 false
583 }
584 }
585}
586
587impl From<LocalEventTimelineItem> for EventTimelineItemKind {
588 fn from(value: LocalEventTimelineItem) -> Self {
589 EventTimelineItemKind::Local(value)
590 }
591}
592
593impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
594 fn from(value: RemoteEventTimelineItem) -> Self {
595 EventTimelineItemKind::Remote(value)
596 }
597}
598
599#[derive(Clone, Debug, Default, PartialEq, Eq)]
601pub struct Profile {
602 pub display_name: Option<String>,
604
605 pub display_name_ambiguous: bool,
611
612 pub avatar_url: Option<OwnedMxcUri>,
614}
615
616#[derive(Clone, Debug)]
620pub enum TimelineDetails<T> {
621 Unavailable,
624
625 Pending,
627
628 Ready(T),
630
631 Error(Arc<Error>),
633}
634
635impl<T> TimelineDetails<T> {
636 pub(crate) fn from_initial_value(value: Option<T>) -> Self {
637 match value {
638 Some(v) => Self::Ready(v),
639 None => Self::Unavailable,
640 }
641 }
642
643 pub fn is_unavailable(&self) -> bool {
644 matches!(self, Self::Unavailable)
645 }
646
647 pub fn is_ready(&self) -> bool {
648 matches!(self, Self::Ready(_))
649 }
650}
651
652#[derive(Clone, Copy, Debug)]
654#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
655pub enum EventItemOrigin {
656 Local,
658 Sync,
660 Pagination,
662 Cache,
664}
665
666#[derive(Clone, Debug)]
668pub enum ReactionStatus {
669 LocalToLocal(Option<SendReactionHandle>),
673 LocalToRemote(Option<SendHandle>),
677 RemoteToRemote(OwnedEventId),
681}
682
683#[derive(Clone, Debug)]
685pub struct ReactionInfo {
686 pub timestamp: MilliSecondsSinceUnixEpoch,
687 pub status: ReactionStatus,
689}
690
691#[derive(Debug, Clone, Default)]
696pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
697
698impl Deref for ReactionsByKeyBySender {
699 type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
700
701 fn deref(&self) -> &Self::Target {
702 &self.0
703 }
704}
705
706impl DerefMut for ReactionsByKeyBySender {
707 fn deref_mut(&mut self) -> &mut Self::Target {
708 &mut self.0
709 }
710}
711
712impl ReactionsByKeyBySender {
713 pub(crate) fn remove_reaction(
719 &mut self,
720 sender: &UserId,
721 annotation: &str,
722 ) -> Option<ReactionInfo> {
723 if let Some(by_user) = self.0.get_mut(annotation)
724 && let Some(info) = by_user.swap_remove(sender)
725 {
726 if by_user.is_empty() {
728 self.0.swap_remove(annotation);
729 }
730 return Some(info);
731 }
732 None
733 }
734}
735
736#[derive(Clone, Copy, Debug, Eq, PartialEq)]
738pub enum TimelineEventShieldState {
739 Red {
742 code: TimelineEventShieldStateCode,
744 },
745 Grey {
748 code: TimelineEventShieldStateCode,
750 },
751 None,
753}
754
755impl From<ShieldState> for TimelineEventShieldState {
756 fn from(value: ShieldState) -> Self {
757 match value {
758 ShieldState::Red { code, message: _ } => {
759 TimelineEventShieldState::Red { code: code.into() }
760 }
761 ShieldState::Grey { code, message: _ } => {
762 TimelineEventShieldState::Grey { code: code.into() }
763 }
764 ShieldState::None => TimelineEventShieldState::None,
765 }
766 }
767}
768
769#[derive(Clone, Copy, Debug, Eq, PartialEq)]
771#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
772pub enum TimelineEventShieldStateCode {
773 AuthenticityNotGuaranteed,
775 UnknownDevice,
777 UnsignedDevice,
779 UnverifiedIdentity,
781 VerificationViolation,
783 MismatchedSender,
786 SentInClear,
788}
789
790impl From<ShieldStateCode> for TimelineEventShieldStateCode {
791 fn from(value: ShieldStateCode) -> Self {
792 use TimelineEventShieldStateCode::*;
793 match value {
794 ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
795 ShieldStateCode::UnknownDevice => UnknownDevice,
796 ShieldStateCode::UnsignedDevice => UnsignedDevice,
797 ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
798 ShieldStateCode::VerificationViolation => VerificationViolation,
799 ShieldStateCode::MismatchedSender => MismatchedSender,
800 }
801 }
802}