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