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