Skip to main content

matrix_sdk_ui/timeline/event_item/
mod.rs

1// Copyright 2022 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// An item in the timeline that represents at least one event.
61///
62/// There is always one main event that gives the `EventTimelineItem` its
63/// identity but in many cases, additional events like reactions and edits are
64/// also part of the item.
65#[derive(Clone, Debug)]
66pub struct EventTimelineItem {
67    /// The sender of the event.
68    pub(super) sender: OwnedUserId,
69    /// The sender's profile of the event.
70    pub(super) sender_profile: TimelineDetails<Profile>,
71    /// If the keys used to decrypt this event were shared-on-invite as part of
72    /// an [MSC4268] key bundle, the user ID of the forwarder.
73    ///
74    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
75    pub(super) forwarder: Option<OwnedUserId>,
76    /// If the keys used to decrypt this event were shared-on-invite as part of
77    /// an [MSC4268] key bundle, the forwarder's profile, if present.
78    ///
79    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
80    pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
81    /// The timestamp of the event.
82    pub(super) timestamp: MilliSecondsSinceUnixEpoch,
83    /// The content of the event. Might be redacted if a redaction for this
84    /// event is currently being sent or has been received from the server.
85    pub(super) content: TimelineItemContent,
86    /// If a redaction for this event is currently being sent but the server
87    /// hasn't yet acknowledged it via its remote echo, the data
88    /// before redaction. This applies to all sorts of timeline items, including
89    /// state events. If no redaction is in flight, None.
90    pub(super) unredacted_item: Option<UnredactedEventTimelineItem>,
91    /// The kind of event timeline item, local or remote.
92    pub(super) kind: EventTimelineItemKind,
93    /// Whether or not the event belongs to an encrypted room.
94    ///
95    /// May be false when we don't know about the room encryption status yet.
96    pub(super) is_room_encrypted: bool,
97}
98
99#[derive(Clone, Debug)]
100pub(super) enum EventTimelineItemKind {
101    /// A local event, not yet echoed back by the server.
102    Local(LocalEventTimelineItem),
103    /// An event received from the server.
104    Remote(RemoteEventTimelineItem),
105}
106
107/// A wrapper that can contain either a transaction id, or an event id.
108#[derive(Clone, Debug, Eq, Hash, PartialEq)]
109pub enum TimelineEventItemId {
110    /// The item is local, identified by its transaction id (to be used in
111    /// subsequent requests).
112    TransactionId(OwnedTransactionId),
113    /// The item is remote, identified by its event id.
114    EventId(OwnedEventId),
115}
116
117/// An handle that usually allows to perform an action on a timeline event.
118///
119/// If the item represents a remote item, then the event id is usually
120/// sufficient to perform an action on it. Otherwise, the send queue handle is
121/// returned, if available.
122pub(crate) enum TimelineItemHandle<'a> {
123    Remote(&'a EventId),
124    Local(&'a SendHandle),
125}
126
127/// A container for temporarily holding onto data that is going to be erased by
128/// a redaction once the server plays it back.
129#[derive(Clone, Debug)]
130pub(super) struct UnredactedEventTimelineItem {
131    /// The original content before redaction.
132    content: TimelineItemContent,
133
134    /// JSON of the original event.
135    pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
136
137    /// JSON of the latest edit to this item.
138    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    /// Check whether this item is a local echo.
167    ///
168    /// This returns `true` for events created locally, until the server echoes
169    /// back the full event as part of a sync response.
170    ///
171    /// This is the opposite of [`Self::is_remote_event`].
172    pub fn is_local_echo(&self) -> bool {
173        matches!(self.kind, EventTimelineItemKind::Local(_))
174    }
175
176    /// Check whether this item is a remote event.
177    ///
178    /// This returns `true` only for events that have been echoed back from the
179    /// homeserver. A local echo sent but not echoed back yet will return
180    /// `false` here.
181    ///
182    /// This is the opposite of [`Self::is_local_echo`].
183    pub fn is_remote_event(&self) -> bool {
184        matches!(self.kind, EventTimelineItemKind::Remote(_))
185    }
186
187    /// Get the `LocalEventTimelineItem` if `self` is `Local`.
188    pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
189        as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
190    }
191
192    /// Get a reference to a [`RemoteEventTimelineItem`] if it's a remote echo.
193    pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
194        as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
195    }
196
197    /// Get a mutable reference to a [`RemoteEventTimelineItem`] if it's a
198    /// remote echo.
199    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    /// Get the event's send state of a local echo.
204    pub fn send_state(&self) -> Option<&EventSendState> {
205        as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
206    }
207
208    /// Get the time that the local event was pushed in the send queue at.
209    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    /// Get the unique identifier of this item.
217    ///
218    /// Returns the transaction ID for a local echo item that has not been sent
219    /// and the event ID for a local echo item that has been sent or a
220    /// remote item.
221    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    /// Get the transaction ID of a local echo item.
231    ///
232    /// The transaction ID is currently only kept until the remote echo for a
233    /// local event is received.
234    pub fn transaction_id(&self) -> Option<&TransactionId> {
235        as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
236    }
237
238    /// Get the event ID of this item.
239    ///
240    /// If this returns `Some(_)`, the event was successfully created by the
241    /// server.
242    ///
243    /// Even if this is a local event, this can be `Some(_)` as the event ID can
244    /// be known not just from the remote echo via `sync_events`, but also
245    /// from the response of the send request that created the event.
246    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    /// Get the sender of this item.
254    pub fn sender(&self) -> &UserId {
255        &self.sender
256    }
257
258    /// Get the profile of the sender.
259    pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
260        &self.sender_profile
261    }
262
263    /// If the keys used to decrypt this event were shared-on-invite as part of
264    /// an [MSC4268] key bundle, returns the user ID of the forwarder.
265    ///
266    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
267    pub fn forwarder(&self) -> Option<&UserId> {
268        self.forwarder.as_deref()
269    }
270
271    /// If the keys used to decrypt this event were shared-on-invite as part of
272    /// an [MSC4268] key bundle, returns the profile of the forwarder.
273    ///
274    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
275    pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
276        self.forwarder_profile.as_ref()
277    }
278
279    /// Get the content of this item.
280    pub fn content(&self) -> &TimelineItemContent {
281        &self.content
282    }
283
284    /// Get a mutable handle to the content of this item.
285    pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
286        &mut self.content
287    }
288
289    /// Get the read receipts of this item.
290    ///
291    /// The key is the ID of a room member and the value are details about the
292    /// read receipt.
293    ///
294    /// Note that currently this ignores threads.
295    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    /// Get the timestamp of this item.
305    ///
306    /// If this event hasn't been echoed back by the server yet, returns the
307    /// time the local event was created. Otherwise, returns the origin
308    /// server timestamp.
309    pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
310        self.timestamp
311    }
312
313    /// Whether this timeline item was sent by the logged-in user themselves.
314    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    /// Flag indicating this timeline item can be edited by the current user.
322    pub fn is_editable(&self) -> bool {
323        // Steps here should be in sync with [`EventTimelineItem::edit_info`] and
324        // [`Timeline::edit_poll`].
325
326        if !self.is_own() {
327            // In theory could work, but it's hard to compute locally.
328            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                // Other MsgLike timeline items can't be edited at the moment.
348                _ => false,
349            },
350            _ => {
351                // Other timeline items can't be edited at the moment.
352                false
353            }
354        }
355    }
356
357    /// Whether the event should be highlighted in the timeline.
358    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    /// Get the encryption information for the event, if any.
366    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    /// Gets the [`TimelineEventShieldState`] which can be used to decorate
374    /// messages in the recommended way.
375    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        // An unable-to-decrypt message has no authenticity shield.
381        if self.content().is_unable_to_decrypt() {
382            return TimelineEventShieldState::None;
383        }
384
385        // A live-location item originates from a `beacon_info` *state* event,
386        // which cannot be encrypted (except with `experimental-encrypted-state-events`
387        // flag). The actual location updates (`beacon` message-like events)
388        // *are* encrypted.
389        //
390        // When there are no beacons yet we return `None` (the state event
391        // itself is inherently unencrypted, so no warning is warranted).
392        // Once at least one beacon has been aggregated, we derive the shield
393        // from the *last* beacon's encryption info so the UI accurately
394        // reflects the authenticity of the most recent location update.
395        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    /// Check whether this item can be replied to.
428    pub fn can_be_replied_to(&self) -> bool {
429        // This must be in sync with the early returns of `Timeline::send_reply`
430        if self.event_id().is_none() {
431            false
432        } else if self.content.is_message() {
433            true
434        } else if self.content().as_live_location_state().is_some() {
435            // Live location sharing session (MSC3489) events are state events, not always
436            // displayed in a timeline, so can't be replied to.
437            false
438        } else {
439            self.latest_json().is_some()
440        }
441    }
442
443    /// Get the raw JSON representation of the initial event (the one that
444    /// caused this timeline item to be created).
445    ///
446    /// Returns `None` if this event hasn't been echoed back by the server
447    /// yet.
448    pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
449        match &self.kind {
450            EventTimelineItemKind::Local(_) => None,
451            EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
452        }
453    }
454
455    /// Get the raw JSON representation of the latest edit, if any.
456    pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
457        match &self.kind {
458            EventTimelineItemKind::Local(_) => None,
459            EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
460        }
461    }
462
463    /// Shorthand for
464    /// `item.latest_edit_json().or_else(|| item.original_json())`.
465    pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
466        self.latest_edit_json().or_else(|| self.original_json())
467    }
468
469    /// Get the origin of the event, i.e. where it came from.
470    ///
471    /// May return `None` in some edge cases that are subject to change.
472    pub fn origin(&self) -> Option<EventItemOrigin> {
473        match &self.kind {
474            EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
475            EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
476                RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
477                RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
478                RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
479                RemoteEventOrigin::Unknown => None,
480            },
481        }
482    }
483
484    pub(super) fn set_content(&mut self, content: TimelineItemContent) {
485        self.content = content;
486    }
487
488    /// Clone the current event item, and update its `kind`.
489    pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
490        Self { kind: kind.into(), ..self.clone() }
491    }
492
493    /// Clone the current event item, and update its content.
494    pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
495        let mut new = self.clone();
496        new.content = new_content;
497        new
498    }
499
500    /// Clone the current event item, and update its content.
501    ///
502    /// Optionally update `latest_edit_json` if the update is an edit received
503    /// from the server.
504    pub(super) fn with_content_and_latest_edit(
505        &self,
506        new_content: TimelineItemContent,
507        edit_json: Option<Raw<AnySyncTimelineEvent>>,
508    ) -> Self {
509        let mut new = self.clone();
510        new.content = new_content;
511        if let EventTimelineItemKind::Remote(r) = &mut new.kind {
512            r.latest_edit_json = edit_json;
513        }
514        new
515    }
516
517    /// Clone the current event item, and update its `sender_profile`.
518    pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
519        Self { sender_profile, ..self.clone() }
520    }
521
522    /// Clone the current event item, and update its `encryption_info`.
523    pub(super) fn with_encryption_info(
524        &self,
525        encryption_info: Option<Arc<EncryptionInfo>>,
526    ) -> Self {
527        let mut new = self.clone();
528        if let EventTimelineItemKind::Remote(r) = &mut new.kind {
529            r.encryption_info = encryption_info;
530        }
531
532        new
533    }
534
535    /// Create a clone of the current item, with content that's been redacted.
536    pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
537        let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
538            content: self.content.clone(),
539            original_json: self.original_json().cloned(),
540            latest_edit_json: self.latest_edit_json().cloned(),
541        });
542        let content = self.content.redact(rules);
543        let kind = match &self.kind {
544            EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
545            EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
546        };
547        Self {
548            sender: self.sender.clone(),
549            sender_profile: self.sender_profile.clone(),
550            forwarder: self.forwarder.clone(),
551            forwarder_profile: self.forwarder_profile.clone(),
552            timestamp: self.timestamp,
553            content,
554            unredacted_item,
555            kind,
556            is_room_encrypted: self.is_room_encrypted,
557        }
558    }
559
560    /// Create a clone of the current item, with data restored from the
561    /// item's unredacted_item field (if it was previously set by a call to
562    /// the `redact(...)` method).
563    pub(super) fn unredact(&self) -> Self {
564        let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
565        let kind = match &self.kind {
566            EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
567            EventTimelineItemKind::Remote(r) => {
568                EventTimelineItemKind::Remote(RemoteEventTimelineItem {
569                    original_json: unredacted_item.original_json.clone(),
570                    latest_edit_json: unredacted_item.latest_edit_json.clone(),
571                    ..r.clone()
572                })
573            }
574        };
575        Self {
576            sender: self.sender.clone(),
577            sender_profile: self.sender_profile.clone(),
578            forwarder: self.forwarder.clone(),
579            forwarder_profile: self.forwarder_profile.clone(),
580            timestamp: self.timestamp,
581            content: unredacted_item.content.clone(),
582            unredacted_item: None,
583            kind,
584            is_room_encrypted: self.is_room_encrypted,
585        }
586    }
587
588    pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
589        match &self.kind {
590            EventTimelineItemKind::Local(local) => {
591                if let Some(event_id) = local.event_id() {
592                    TimelineItemHandle::Remote(event_id)
593                } else {
594                    TimelineItemHandle::Local(
595                        // The send_handle must always be present, except in tests.
596                        local.send_handle.as_ref().expect("Unexpected missing send_handle"),
597                    )
598                }
599            }
600            EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
601        }
602    }
603
604    /// For local echoes, return the associated send handle.
605    pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
606        as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
607    }
608
609    /// Some clients may want to know if a particular text message or media
610    /// caption contains only emojis so that they can render them bigger for
611    /// added effect.
612    ///
613    /// This function provides that feature with the following
614    /// behavior/limitations:
615    /// - ignores leading and trailing white spaces
616    /// - fails texts bigger than 5 graphemes for performance reasons
617    /// - checks the body only for [`MessageType::Text`]
618    /// - only checks the caption for [`MessageType::Audio`],
619    ///   [`MessageType::File`], [`MessageType::Image`], and
620    ///   [`MessageType::Video`] if present
621    /// - all other message types will not match
622    ///
623    /// # Examples
624    /// # fn render_timeline_item(timeline_item: TimelineItem) {
625    /// if timeline_item.contains_only_emojis() {
626    ///     // e.g. increase the font size
627    /// }
628    /// # }
629    ///
630    /// See `test_emoji_detection` for more examples.
631    pub fn contains_only_emojis(&self) -> bool {
632        let body = match self.content() {
633            TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
634                MsgLikeKind::Message(message) => match &message.msgtype {
635                    MessageType::Text(text) => Some(text.body.as_str()),
636                    MessageType::Audio(audio) => audio.caption(),
637                    MessageType::File(file) => file.caption(),
638                    MessageType::Image(image) => image.caption(),
639                    MessageType::Video(video) => video.caption(),
640                    _ => None,
641                },
642                MsgLikeKind::Sticker(_)
643                | MsgLikeKind::Poll(_)
644                | MsgLikeKind::Redacted
645                | MsgLikeKind::UnableToDecrypt(_)
646                | MsgLikeKind::Other(_)
647                | MsgLikeKind::LiveLocation(_) => None,
648            },
649            TimelineItemContent::MembershipChange(_)
650            | TimelineItemContent::ProfileChange(_)
651            | TimelineItemContent::OtherState(_)
652            | TimelineItemContent::FailedToParseMessageLike { .. }
653            | TimelineItemContent::FailedToParseState { .. }
654            | TimelineItemContent::CallInvite
655            | TimelineItemContent::RtcNotification { .. } => None,
656        };
657
658        if let Some(body) = body {
659            // Collect the graphemes after trimming white spaces.
660            let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
661
662            // Limit the check to 5 graphemes for performance and security
663            // reasons. This will probably be used for every new message so we
664            // want it to be fast and we don't want to allow a DoS attack by
665            // sending a huge message.
666            if graphemes.len() > 5 {
667                return false;
668            }
669
670            graphemes.iter().all(|g| emojis::get(g).is_some())
671        } else {
672            false
673        }
674    }
675}
676
677impl From<LocalEventTimelineItem> for EventTimelineItemKind {
678    fn from(value: LocalEventTimelineItem) -> Self {
679        EventTimelineItemKind::Local(value)
680    }
681}
682
683impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
684    fn from(value: RemoteEventTimelineItem) -> Self {
685        EventTimelineItemKind::Remote(value)
686    }
687}
688
689/// The display name and avatar URL of a room member.
690#[derive(Clone, Debug, Default, PartialEq, Eq)]
691pub struct Profile {
692    /// The display name, if set.
693    pub display_name: Option<String>,
694
695    /// Whether the display name is ambiguous.
696    ///
697    /// Note that in rooms with lazy-loading enabled, this could be `false` even
698    /// though the display name is actually ambiguous if not all member events
699    /// have been seen yet.
700    pub display_name_ambiguous: bool,
701
702    /// The avatar URL, if set.
703    pub avatar_url: Option<OwnedMxcUri>,
704}
705
706impl Profile {
707    pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
708        match room.get_member_no_sync(user_id).await {
709            Ok(Some(member)) => Some(Profile {
710                display_name: member.display_name().map(ToOwned::to_owned),
711                display_name_ambiguous: member.name_ambiguous(),
712                avatar_url: member.avatar_url().map(ToOwned::to_owned),
713            }),
714            Ok(None) if room.are_members_synced() => Some(Profile::default()),
715            Ok(None) => None,
716            Err(e) => {
717                error!(%user_id, "Failed to fetch room member information: {e}");
718                None
719            }
720        }
721    }
722}
723
724/// Some details of an [`EventTimelineItem`] that may require server requests
725/// other than just the regular
726/// [`sync_events`][ruma::api::client::sync::sync_events].
727#[derive(Clone, Debug)]
728pub enum TimelineDetails<T> {
729    /// The details are not available yet, and have not been requested from the
730    /// server.
731    Unavailable,
732
733    /// The details are not available yet, but have been requested.
734    Pending,
735
736    /// The details are available.
737    Ready(T),
738
739    /// An error occurred when fetching the details.
740    Error(Arc<Error>),
741}
742
743impl<T> TimelineDetails<T> {
744    /// Create a [`TimelineDetails`] from an initial value that may or may not
745    /// be available.
746    ///
747    /// Will be [`TimelineDetails::Ready`] if the value is `Some(_)`, and
748    /// [`TimelineDetails::Unavailable`] if the value is `None`.
749    pub fn from_initial_value(value: Option<T>) -> Self {
750        match value {
751            Some(v) => Self::Ready(v),
752            None => Self::Unavailable,
753        }
754    }
755
756    pub fn is_unavailable(&self) -> bool {
757        matches!(self, Self::Unavailable)
758    }
759
760    pub fn is_ready(&self) -> bool {
761        matches!(self, Self::Ready(_))
762    }
763}
764
765/// Where this event came.
766#[derive(Clone, Copy, Debug)]
767#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
768pub enum EventItemOrigin {
769    /// The event was created locally.
770    Local,
771    /// The event came from a sync response.
772    Sync,
773    /// The event came from pagination.
774    Pagination,
775    /// The event came from a cache.
776    Cache,
777}
778
779/// What's the status of a reaction?
780#[derive(Clone, Debug)]
781pub enum ReactionStatus {
782    /// It's a local reaction to a local echo.
783    ///
784    /// The handle is missing only in testing contexts.
785    LocalToLocal(Option<SendReactionHandle>),
786    /// It's a local reaction to a remote event.
787    ///
788    /// The handle is missing only in testing contexts.
789    LocalToRemote(Option<SendHandle>),
790    /// It's a remote reaction to a remote event.
791    ///
792    /// The event id is that of the reaction event (not the target event).
793    RemoteToRemote(OwnedEventId),
794}
795
796/// Information about a single reaction stored in [`ReactionsByKeyBySender`].
797#[derive(Clone, Debug)]
798pub struct ReactionInfo {
799    pub timestamp: MilliSecondsSinceUnixEpoch,
800    /// Current status of this reaction.
801    pub status: ReactionStatus,
802}
803
804/// Reactions grouped by key first, then by sender.
805///
806/// This representation makes sure that a given sender has sent at most one
807/// reaction for an event.
808#[derive(Debug, Clone, Default)]
809pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
810
811impl Deref for ReactionsByKeyBySender {
812    type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
813
814    fn deref(&self) -> &Self::Target {
815        &self.0
816    }
817}
818
819impl DerefMut for ReactionsByKeyBySender {
820    fn deref_mut(&mut self) -> &mut Self::Target {
821        &mut self.0
822    }
823}
824
825impl ReactionsByKeyBySender {
826    /// Removes (in place) a reaction from the sender with the given annotation
827    /// from the mapping.
828    ///
829    /// Returns true if the reaction was found and thus removed, false
830    /// otherwise.
831    pub(crate) fn remove_reaction(
832        &mut self,
833        sender: &UserId,
834        annotation: &str,
835    ) -> Option<ReactionInfo> {
836        if let Some(by_user) = self.0.get_mut(annotation)
837            && let Some(info) = by_user.swap_remove(sender)
838        {
839            // If this was the last reaction, remove the annotation entry.
840            if by_user.is_empty() {
841                self.0.swap_remove(annotation);
842            }
843            return Some(info);
844        }
845        None
846    }
847}
848
849/// Extends [`ShieldState`] to allow for a `SentInClear` code.
850#[derive(Clone, Copy, Debug, Eq, PartialEq)]
851pub enum TimelineEventShieldState {
852    /// A red shield with a tooltip containing a message appropriate to the
853    /// associated code should be presented.
854    Red {
855        /// A machine-readable representation.
856        code: TimelineEventShieldStateCode,
857    },
858    /// A grey shield with a tooltip containing a message appropriate to the
859    /// associated code should be presented.
860    Grey {
861        /// A machine-readable representation.
862        code: TimelineEventShieldStateCode,
863    },
864    /// No shield should be presented.
865    None,
866}
867
868impl From<ShieldState> for TimelineEventShieldState {
869    fn from(value: ShieldState) -> Self {
870        match value {
871            ShieldState::Red { code, message: _ } => {
872                TimelineEventShieldState::Red { code: code.into() }
873            }
874            ShieldState::Grey { code, message: _ } => {
875                TimelineEventShieldState::Grey { code: code.into() }
876            }
877            ShieldState::None => TimelineEventShieldState::None,
878        }
879    }
880}
881
882/// Extends [`ShieldStateCode`] to allow for a `SentInClear` code.
883#[derive(Clone, Copy, Debug, Eq, PartialEq)]
884#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
885pub enum TimelineEventShieldStateCode {
886    /// Not enough information available to check the authenticity.
887    AuthenticityNotGuaranteed,
888    /// The sending device isn't yet known by the Client.
889    UnknownDevice,
890    /// The sending device hasn't been verified by the sender.
891    UnsignedDevice,
892    /// The sender hasn't been verified by the Client's user.
893    UnverifiedIdentity,
894    /// The sender was previously verified but changed their identity.
895    VerificationViolation,
896    /// The `sender` field on the event does not match the owner of the device
897    /// that established the Megolm session.
898    MismatchedSender,
899    /// An unencrypted event in an encrypted room.
900    SentInClear,
901}
902
903impl From<ShieldStateCode> for TimelineEventShieldStateCode {
904    fn from(value: ShieldStateCode) -> Self {
905        use TimelineEventShieldStateCode::*;
906        match value {
907            ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
908            ShieldStateCode::UnknownDevice => UnknownDevice,
909            ShieldStateCode::UnsignedDevice => UnsignedDevice,
910            ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
911            ShieldStateCode::VerificationViolation => VerificationViolation,
912            ShieldStateCode::MismatchedSender => MismatchedSender,
913        }
914    }
915}
916
917#[cfg(test)]
918mod tests {
919    use std::time::Duration;
920
921    use ruma::{
922        MilliSecondsSinceUnixEpoch,
923        events::{
924            AnySyncTimelineEvent,
925            beacon_info::BeaconInfoEventContent,
926            room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
927        },
928        owned_event_id, owned_user_id,
929        serde::Raw,
930        uint,
931    };
932    use serde_json::json;
933
934    use super::{
935        EventSendState, EventTimelineItem, EventTimelineItemKind, LiveLocationState,
936        LocalEventTimelineItem, Message, MsgLikeContent, MsgLikeKind, RemoteEventOrigin,
937        RemoteEventTimelineItem, TimelineDetails, TimelineItemContent,
938    };
939
940    fn message_content() -> TimelineItemContent {
941        TimelineItemContent::MsgLike(MsgLikeContent {
942            kind: MsgLikeKind::Message(Message {
943                msgtype: MessageType::Text(TextMessageEventContent::plain("hello")),
944                edited: false,
945                mentions: None,
946            }),
947            reactions: Default::default(),
948            thread_root: None,
949            in_reply_to: None,
950            thread_summary: None,
951        })
952    }
953
954    fn live_location_content() -> TimelineItemContent {
955        TimelineItemContent::MsgLike(MsgLikeContent {
956            kind: MsgLikeKind::LiveLocation(LiveLocationState::new(BeaconInfoEventContent::new(
957                None,
958                Duration::from_secs(300),
959                true,
960                Some(MilliSecondsSinceUnixEpoch(uint!(1))),
961            ))),
962            reactions: Default::default(),
963            thread_root: None,
964            in_reply_to: None,
965            thread_summary: None,
966        })
967    }
968
969    fn remote_item(
970        content: TimelineItemContent,
971        original_json: Option<Raw<AnySyncTimelineEvent>>,
972    ) -> EventTimelineItem {
973        EventTimelineItem::new(
974            owned_user_id!("@alice:example.org"),
975            TimelineDetails::Unavailable,
976            None,
977            None,
978            MilliSecondsSinceUnixEpoch(uint!(1)),
979            content,
980            EventTimelineItemKind::Remote(RemoteEventTimelineItem {
981                event_id: owned_event_id!("$event"),
982                transaction_id: None,
983                read_receipts: Default::default(),
984                is_own: false,
985                is_highlighted: false,
986                encryption_info: None,
987                original_json,
988                latest_edit_json: None,
989                origin: RemoteEventOrigin::Sync,
990            }),
991            false,
992        )
993    }
994
995    fn local_unsent_item(content: TimelineItemContent) -> EventTimelineItem {
996        EventTimelineItem::new(
997            owned_user_id!("@alice:example.org"),
998            TimelineDetails::Unavailable,
999            None,
1000            None,
1001            MilliSecondsSinceUnixEpoch(uint!(1)),
1002            content,
1003            EventTimelineItemKind::Local(LocalEventTimelineItem {
1004                send_state: EventSendState::NotSentYet { progress: None },
1005                transaction_id: "t0".into(),
1006                send_handle: None,
1007            }),
1008            false,
1009        )
1010    }
1011
1012    fn sample_raw_event() -> Raw<AnySyncTimelineEvent> {
1013        Raw::from_json_string(
1014            json!({
1015                "content": RoomMessageEventContent::text_plain("hi"),
1016                "type": "m.room.message",
1017                "event_id": "$event",
1018                "room_id": "!room:example.org",
1019                "origin_server_ts": 1,
1020                "sender": "@alice:example.org",
1021            })
1022            .to_string(),
1023        )
1024        .unwrap()
1025    }
1026
1027    #[test]
1028    fn cannot_reply_to_local_unsent_events() {
1029        let item = local_unsent_item(message_content());
1030        assert!(!item.can_be_replied_to());
1031    }
1032
1033    #[test]
1034    fn can_reply_to_messages() {
1035        let item = remote_item(message_content(), None);
1036        assert!(item.can_be_replied_to());
1037    }
1038
1039    #[test]
1040    fn cannot_reply_to_live_location_events() {
1041        let item = remote_item(live_location_content(), Some(sample_raw_event()));
1042        assert!(!item.can_be_replied_to());
1043    }
1044
1045    #[test]
1046    fn cannot_reply_to_non_messages_with_no_json() {
1047        let item = remote_item(TimelineItemContent::CallInvite, None);
1048        assert!(!item.can_be_replied_to());
1049    }
1050
1051    #[test]
1052    fn can_reply_to_non_messages_with_json() {
1053        let item = remote_item(TimelineItemContent::CallInvite, Some(sample_raw_event()));
1054        assert!(item.can_be_replied_to());
1055    }
1056}