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