1use std::{
16 ops::{Deref, DerefMut},
17 sync::Arc,
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 once_cell::sync::Lazy;
29use ruma::{
30 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
31 OwnedUserId, TransactionId, UserId,
32 events::{AnySyncTimelineEvent, receipt::Receipt, room::message::MessageType},
33 room_version_rules::RedactionRules,
34 serde::Raw,
35};
36use unicode_segmentation::UnicodeSegmentation;
37
38mod content;
39mod local;
40mod remote;
41
42pub use self::{
43 content::{
44 AnyOtherFullStateEventContent, EmbeddedEvent, EncryptedMessage, InReplyToDetails,
45 MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind,
46 OtherMessageLike, OtherState, PollResult, PollState, RoomMembershipChange,
47 RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
48 },
49 local::{EventSendState, MediaUploadProgress},
50};
51pub(super) use self::{
52 content::{
53 extract_bundled_edit_event_json, extract_poll_edit_content, 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: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::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}