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