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