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