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,
18};
19
20use as_variant::as_variant;
21use indexmap::IndexMap;
22use matrix_sdk::{
23    deserialized_responses::{EncryptionInfo, ShieldState},
24    send_queue::{SendHandle, SendReactionHandle},
25    Client, Error,
26};
27use matrix_sdk_base::{
28    deserialized_responses::{ShieldStateCode, SENT_IN_CLEAR},
29    latest_event::LatestEvent,
30};
31use once_cell::sync::Lazy;
32use ruma::{
33    events::{receipt::Receipt, room::message::MessageType, AnySyncTimelineEvent},
34    serde::Raw,
35    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
36    OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
37};
38use tracing::warn;
39use unicode_segmentation::UnicodeSegmentation;
40
41mod content;
42mod local;
43mod remote;
44
45pub(super) use self::{
46    content::{
47        extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
48    },
49    local::LocalEventTimelineItem,
50    remote::{RemoteEventOrigin, RemoteEventTimelineItem},
51};
52pub use self::{
53    content::{
54        AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange,
55        MembershipChange, Message, MsgLikeContent, MsgLikeKind, OtherState, PollResult, PollState,
56        RepliedToEvent, RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineItemContent,
57    },
58    local::EventSendState,
59};
60
61/// An item in the timeline that represents at least one event.
62///
63/// There is always one main event that gives the `EventTimelineItem` its
64/// identity but in many cases, additional events like reactions and edits are
65/// also part of the item.
66#[derive(Clone, Debug)]
67pub struct EventTimelineItem {
68    /// The sender of the event.
69    pub(super) sender: OwnedUserId,
70    /// The sender's profile of the event.
71    pub(super) sender_profile: TimelineDetails<Profile>,
72    /// The timestamp of the event.
73    pub(super) timestamp: MilliSecondsSinceUnixEpoch,
74    /// The content of the event.
75    pub(super) content: TimelineItemContent,
76    /// The kind of event timeline item, local or remote.
77    pub(super) kind: EventTimelineItemKind,
78    /// Whether or not the event belongs to an encrypted room.
79    ///
80    /// May be false when we don't know about the room encryption status yet.
81    pub(super) is_room_encrypted: bool,
82}
83
84#[derive(Clone, Debug)]
85pub(super) enum EventTimelineItemKind {
86    /// A local event, not yet echoed back by the server.
87    Local(LocalEventTimelineItem),
88    /// An event received from the server.
89    Remote(RemoteEventTimelineItem),
90}
91
92/// A wrapper that can contain either a transaction id, or an event id.
93#[derive(Clone, Debug, Eq, Hash, PartialEq)]
94pub enum TimelineEventItemId {
95    /// The item is local, identified by its transaction id (to be used in
96    /// subsequent requests).
97    TransactionId(OwnedTransactionId),
98    /// The item is remote, identified by its event id.
99    EventId(OwnedEventId),
100}
101
102/// An handle that usually allows to perform an action on a timeline event.
103///
104/// If the item represents a remote item, then the event id is usually
105/// sufficient to perform an action on it. Otherwise, the send queue handle is
106/// returned, if available.
107pub(crate) enum TimelineItemHandle<'a> {
108    Remote(&'a EventId),
109    Local(&'a SendHandle),
110}
111
112impl EventTimelineItem {
113    pub(super) fn new(
114        sender: OwnedUserId,
115        sender_profile: TimelineDetails<Profile>,
116        timestamp: MilliSecondsSinceUnixEpoch,
117        content: TimelineItemContent,
118        kind: EventTimelineItemKind,
119        is_room_encrypted: bool,
120    ) -> Self {
121        Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted }
122    }
123
124    /// If the supplied low-level [`TimelineEvent`] is suitable for use as the
125    /// `latest_event` in a message preview, wrap it as an
126    /// `EventTimelineItem`.
127    ///
128    /// **Note:** Timeline items created via this constructor do **not** produce
129    /// the correct ShieldState when calling
130    /// [`get_shield`][EventTimelineItem::get_shield]. This is because they are
131    /// intended for display in the room list which a) is unlikely to show
132    /// shields and b) would incur a significant performance overhead.
133    ///
134    /// [`TimelineEvent`]: matrix_sdk::deserialized_responses::TimelineEvent
135    pub async fn from_latest_event(
136        client: Client,
137        room_id: &RoomId,
138        latest_event: LatestEvent,
139    ) -> Option<EventTimelineItem> {
140        // TODO: We shouldn't be returning an EventTimelineItem here because we're
141        // starting to diverge on what kind of data we need. The note above is a
142        // potential footgun which could one day turn into a security issue.
143        use super::traits::RoomDataProvider;
144
145        let raw_sync_event = latest_event.event().raw().clone();
146        let encryption_info = latest_event.event().encryption_info().cloned();
147
148        let Ok(event) = raw_sync_event.deserialize_as::<AnySyncTimelineEvent>() else {
149            warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!");
150            return None;
151        };
152
153        let timestamp = event.origin_server_ts();
154        let sender = event.sender().to_owned();
155        let event_id = event.event_id().to_owned();
156        let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false);
157
158        // Get the room's power levels for calculating the latest event
159        let power_levels = if let Some(room) = client.get_room(room_id) {
160            room.power_levels().await.ok()
161        } else {
162            None
163        };
164        let room_power_levels_info = client.user_id().zip(power_levels.as_ref());
165
166        // If we don't (yet) know how to handle this type of message, return `None`
167        // here. If we do, convert it into a `TimelineItemContent`.
168        let content =
169            TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?;
170
171        // The message preview probably never needs read receipts.
172        let read_receipts = IndexMap::new();
173
174        // Being highlighted is _probably_ not relevant to the message preview.
175        let is_highlighted = false;
176
177        // We may need this, depending on how we are going to display edited messages in
178        // previews.
179        let latest_edit_json = None;
180
181        // Probably the origin of the event doesn't matter for the preview.
182        let origin = RemoteEventOrigin::Sync;
183
184        let kind = RemoteEventTimelineItem {
185            event_id,
186            transaction_id: None,
187            read_receipts,
188            is_own,
189            is_highlighted,
190            encryption_info,
191            original_json: Some(raw_sync_event),
192            latest_edit_json,
193            origin,
194        }
195        .into();
196
197        let room = client.get_room(room_id);
198        let sender_profile = if let Some(room) = room {
199            let mut profile = room.profile_from_latest_event(&latest_event);
200
201            // Fallback to the slow path.
202            if profile.is_none() {
203                profile = room.profile_from_user_id(&sender).await;
204            }
205
206            profile.map(TimelineDetails::Ready).unwrap_or(TimelineDetails::Unavailable)
207        } else {
208            TimelineDetails::Unavailable
209        };
210
211        Some(Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted: false })
212    }
213
214    /// Check whether this item is a local echo.
215    ///
216    /// This returns `true` for events created locally, until the server echoes
217    /// back the full event as part of a sync response.
218    ///
219    /// This is the opposite of [`Self::is_remote_event`].
220    pub fn is_local_echo(&self) -> bool {
221        matches!(self.kind, EventTimelineItemKind::Local(_))
222    }
223
224    /// Check whether this item is a remote event.
225    ///
226    /// This returns `true` only for events that have been echoed back from the
227    /// homeserver. A local echo sent but not echoed back yet will return
228    /// `false` here.
229    ///
230    /// This is the opposite of [`Self::is_local_echo`].
231    pub fn is_remote_event(&self) -> bool {
232        matches!(self.kind, EventTimelineItemKind::Remote(_))
233    }
234
235    /// Get the `LocalEventTimelineItem` if `self` is `Local`.
236    pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
237        as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
238    }
239
240    /// Get a reference to a [`RemoteEventTimelineItem`] if it's a remote echo.
241    pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
242        as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
243    }
244
245    /// Get a mutable reference to a [`RemoteEventTimelineItem`] if it's a
246    /// remote echo.
247    pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
248        as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
249    }
250
251    /// Get the event's send state of a local echo.
252    pub fn send_state(&self) -> Option<&EventSendState> {
253        as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
254    }
255
256    /// Get the time that the local event was pushed in the send queue at.
257    pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
258        match &self.kind {
259            EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
260            EventTimelineItemKind::Remote(_) => None,
261        }
262    }
263
264    /// Get the unique identifier of this item.
265    ///
266    /// Returns the transaction ID for a local echo item that has not been sent
267    /// and the event ID for a local echo item that has been sent or a
268    /// remote item.
269    pub fn identifier(&self) -> TimelineEventItemId {
270        match &self.kind {
271            EventTimelineItemKind::Local(local) => local.identifier(),
272            EventTimelineItemKind::Remote(remote) => {
273                TimelineEventItemId::EventId(remote.event_id.clone())
274            }
275        }
276    }
277
278    /// Get the transaction ID of a local echo item.
279    ///
280    /// The transaction ID is currently only kept until the remote echo for a
281    /// local event is received.
282    pub fn transaction_id(&self) -> Option<&TransactionId> {
283        as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
284    }
285
286    /// Get the event ID of this item.
287    ///
288    /// If this returns `Some(_)`, the event was successfully created by the
289    /// server.
290    ///
291    /// Even if this is a local event, this can be `Some(_)` as the event ID can
292    /// be known not just from the remote echo via `sync_events`, but also
293    /// from the response of the send request that created the event.
294    pub fn event_id(&self) -> Option<&EventId> {
295        match &self.kind {
296            EventTimelineItemKind::Local(local_event) => local_event.event_id(),
297            EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
298        }
299    }
300
301    /// Get the sender of this item.
302    pub fn sender(&self) -> &UserId {
303        &self.sender
304    }
305
306    /// Get the profile of the sender.
307    pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
308        &self.sender_profile
309    }
310
311    /// Get the content of this item.
312    pub fn content(&self) -> &TimelineItemContent {
313        &self.content
314    }
315
316    /// Get the read receipts of this item.
317    ///
318    /// The key is the ID of a room member and the value are details about the
319    /// read receipt.
320    ///
321    /// Note that currently this ignores threads.
322    pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
323        static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
324        match &self.kind {
325            EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
326            EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
327        }
328    }
329
330    /// Get the timestamp of this item.
331    ///
332    /// If this event hasn't been echoed back by the server yet, returns the
333    /// time the local event was created. Otherwise, returns the origin
334    /// server timestamp.
335    pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
336        self.timestamp
337    }
338
339    /// Whether this timeline item was sent by the logged-in user themselves.
340    pub fn is_own(&self) -> bool {
341        match &self.kind {
342            EventTimelineItemKind::Local(_) => true,
343            EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
344        }
345    }
346
347    /// Flag indicating this timeline item can be edited by the current user.
348    pub fn is_editable(&self) -> bool {
349        // Steps here should be in sync with [`EventTimelineItem::edit_info`] and
350        // [`Timeline::edit_poll`].
351
352        if !self.is_own() {
353            // In theory could work, but it's hard to compute locally.
354            return false;
355        }
356
357        match self.content() {
358            TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
359                MsgLikeKind::Message(message) => {
360                    matches!(
361                        message.msgtype(),
362                        MessageType::Text(_)
363                            | MessageType::Emote(_)
364                            | MessageType::Audio(_)
365                            | MessageType::File(_)
366                            | MessageType::Image(_)
367                            | MessageType::Video(_)
368                    )
369                }
370                MsgLikeKind::Poll(poll) => {
371                    poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
372                }
373                // Other MsgLike timeline items can't be edited at the moment.
374                _ => false,
375            },
376            _ => {
377                // Other timeline items can't be edited at the moment.
378                false
379            }
380        }
381    }
382
383    /// Whether the event should be highlighted in the timeline.
384    pub fn is_highlighted(&self) -> bool {
385        match &self.kind {
386            EventTimelineItemKind::Local(_) => false,
387            EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
388        }
389    }
390
391    /// Get the encryption information for the event, if any.
392    pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
393        match &self.kind {
394            EventTimelineItemKind::Local(_) => None,
395            EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_ref(),
396        }
397    }
398
399    /// Gets the [`ShieldState`] which can be used to decorate messages in the
400    /// recommended way.
401    pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
402        if !self.is_room_encrypted || self.is_local_echo() {
403            return None;
404        }
405
406        // An unable-to-decrypt message has no authenticity shield.
407        if self.content().is_unable_to_decrypt() {
408            return None;
409        }
410
411        match self.encryption_info() {
412            Some(info) => {
413                if strict {
414                    Some(info.verification_state.to_shield_state_strict())
415                } else {
416                    Some(info.verification_state.to_shield_state_lax())
417                }
418            }
419            None => Some(ShieldState::Red {
420                code: ShieldStateCode::SentInClear,
421                message: SENT_IN_CLEAR,
422            }),
423        }
424    }
425
426    /// Check whether this item can be replied to.
427    pub fn can_be_replied_to(&self) -> bool {
428        // This must be in sync with the early returns of `Timeline::send_reply`
429        if self.event_id().is_none() {
430            false
431        } else if self.content.is_message() {
432            true
433        } else {
434            self.latest_json().is_some()
435        }
436    }
437
438    /// Get the raw JSON representation of the initial event (the one that
439    /// caused this timeline item to be created).
440    ///
441    /// Returns `None` if this event hasn't been echoed back by the server
442    /// yet.
443    pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
444        match &self.kind {
445            EventTimelineItemKind::Local(_) => None,
446            EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
447        }
448    }
449
450    /// Get the raw JSON representation of the latest edit, if any.
451    pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
452        match &self.kind {
453            EventTimelineItemKind::Local(_) => None,
454            EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
455        }
456    }
457
458    /// Shorthand for
459    /// `item.latest_edit_json().or_else(|| item.original_json())`.
460    pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
461        self.latest_edit_json().or_else(|| self.original_json())
462    }
463
464    /// Get the origin of the event, i.e. where it came from.
465    ///
466    /// May return `None` in some edge cases that are subject to change.
467    pub fn origin(&self) -> Option<EventItemOrigin> {
468        match &self.kind {
469            EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
470            EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
471                RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
472                RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
473                RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
474                RemoteEventOrigin::Unknown => None,
475            },
476        }
477    }
478
479    pub(super) fn set_content(&mut self, content: TimelineItemContent) {
480        self.content = content;
481    }
482
483    /// Clone the current event item, and update its `kind`.
484    pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
485        Self { kind: kind.into(), ..self.clone() }
486    }
487
488    /// Clone the current event item, and update its content.
489    pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
490        let mut new = self.clone();
491        new.content = new_content;
492        new
493    }
494
495    /// Clone the current event item, and update its content.
496    ///
497    /// Optionally update `latest_edit_json` if the update is an edit received
498    /// from the server.
499    pub(super) fn with_content_and_latest_edit(
500        &self,
501        new_content: TimelineItemContent,
502        edit_json: Option<Raw<AnySyncTimelineEvent>>,
503    ) -> Self {
504        let mut new = self.clone();
505        new.content = new_content;
506        if let EventTimelineItemKind::Remote(r) = &mut new.kind {
507            r.latest_edit_json = edit_json;
508        }
509        new
510    }
511
512    /// Clone the current event item, and update its `sender_profile`.
513    pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
514        Self { sender_profile, ..self.clone() }
515    }
516
517    /// Clone the current event item, and update its `encryption_info`.
518    pub(super) fn with_encryption_info(&self, encryption_info: Option<EncryptionInfo>) -> Self {
519        let mut new = self.clone();
520        if let EventTimelineItemKind::Remote(r) = &mut new.kind {
521            r.encryption_info = encryption_info;
522        }
523
524        new
525    }
526
527    /// Create a clone of the current item, with content that's been redacted.
528    pub(super) fn redact(&self, room_version: &RoomVersionId) -> Self {
529        let content = self.content.redact(room_version);
530        let kind = match &self.kind {
531            EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
532            EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
533        };
534        Self {
535            sender: self.sender.clone(),
536            sender_profile: self.sender_profile.clone(),
537            timestamp: self.timestamp,
538            content,
539            kind,
540            is_room_encrypted: self.is_room_encrypted,
541        }
542    }
543
544    pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
545        match &self.kind {
546            EventTimelineItemKind::Local(local) => {
547                if let Some(event_id) = local.event_id() {
548                    TimelineItemHandle::Remote(event_id)
549                } else {
550                    TimelineItemHandle::Local(
551                        // The send_handle must always be present, except in tests.
552                        local.send_handle.as_ref().expect("Unexpected missing send_handle"),
553                    )
554                }
555            }
556            EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
557        }
558    }
559
560    /// For local echoes, return the associated send handle.
561    pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
562        as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
563    }
564
565    /// Some clients may want to know if a particular text message or media
566    /// caption contains only emojis so that they can render them bigger for
567    /// added effect.
568    ///
569    /// This function provides that feature with the following
570    /// behavior/limitations:
571    /// - ignores leading and trailing white spaces
572    /// - fails texts bigger than 5 graphemes for performance reasons
573    /// - checks the body only for [`MessageType::Text`]
574    /// - only checks the caption for [`MessageType::Audio`],
575    ///   [`MessageType::File`], [`MessageType::Image`], and
576    ///   [`MessageType::Video`] if present
577    /// - all other message types will not match
578    ///
579    /// # Examples
580    /// # fn render_timeline_item(timeline_item: TimelineItem) {
581    /// if timeline_item.contains_only_emojis() {
582    ///     // e.g. increase the font size
583    /// }
584    /// # }
585    ///
586    /// See `test_emoji_detection` for more examples.
587    pub fn contains_only_emojis(&self) -> bool {
588        let body = match self.content() {
589            TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
590                MsgLikeKind::Message(message) => match &message.msgtype {
591                    MessageType::Text(text) => Some(text.body.as_str()),
592                    MessageType::Audio(audio) => audio.caption(),
593                    MessageType::File(file) => file.caption(),
594                    MessageType::Image(image) => image.caption(),
595                    MessageType::Video(video) => video.caption(),
596                    _ => None,
597                },
598                MsgLikeKind::Sticker(_)
599                | MsgLikeKind::Poll(_)
600                | MsgLikeKind::Redacted
601                | MsgLikeKind::UnableToDecrypt(_) => None,
602            },
603            TimelineItemContent::MembershipChange(_)
604            | TimelineItemContent::ProfileChange(_)
605            | TimelineItemContent::OtherState(_)
606            | TimelineItemContent::FailedToParseMessageLike { .. }
607            | TimelineItemContent::FailedToParseState { .. }
608            | TimelineItemContent::CallInvite
609            | TimelineItemContent::CallNotify => None,
610        };
611
612        if let Some(body) = body {
613            // Collect the graphemes after trimming white spaces.
614            let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
615
616            // Limit the check to 5 graphemes for performance and security
617            // reasons. This will probably be used for every new message so we
618            // want it to be fast and we don't want to allow a DoS attack by
619            // sending a huge message.
620            if graphemes.len() > 5 {
621                return false;
622            }
623
624            graphemes.iter().all(|g| emojis::get(g).is_some())
625        } else {
626            false
627        }
628    }
629}
630
631impl From<LocalEventTimelineItem> for EventTimelineItemKind {
632    fn from(value: LocalEventTimelineItem) -> Self {
633        EventTimelineItemKind::Local(value)
634    }
635}
636
637impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
638    fn from(value: RemoteEventTimelineItem) -> Self {
639        EventTimelineItemKind::Remote(value)
640    }
641}
642
643/// The display name and avatar URL of a room member.
644#[derive(Clone, Debug, Default, PartialEq, Eq)]
645pub struct Profile {
646    /// The display name, if set.
647    pub display_name: Option<String>,
648
649    /// Whether the display name is ambiguous.
650    ///
651    /// Note that in rooms with lazy-loading enabled, this could be `false` even
652    /// though the display name is actually ambiguous if not all member events
653    /// have been seen yet.
654    pub display_name_ambiguous: bool,
655
656    /// The avatar URL, if set.
657    pub avatar_url: Option<OwnedMxcUri>,
658}
659
660/// Some details of an [`EventTimelineItem`] that may require server requests
661/// other than just the regular
662/// [`sync_events`][ruma::api::client::sync::sync_events].
663#[derive(Clone, Debug)]
664pub enum TimelineDetails<T> {
665    /// The details are not available yet, and have not been requested from the
666    /// server.
667    Unavailable,
668
669    /// The details are not available yet, but have been requested.
670    Pending,
671
672    /// The details are available.
673    Ready(T),
674
675    /// An error occurred when fetching the details.
676    Error(Arc<Error>),
677}
678
679impl<T> TimelineDetails<T> {
680    pub(crate) fn from_initial_value(value: Option<T>) -> Self {
681        match value {
682            Some(v) => Self::Ready(v),
683            None => Self::Unavailable,
684        }
685    }
686
687    pub(crate) fn is_unavailable(&self) -> bool {
688        matches!(self, Self::Unavailable)
689    }
690
691    pub fn is_ready(&self) -> bool {
692        matches!(self, Self::Ready(_))
693    }
694}
695
696/// Where this event came.
697#[derive(Clone, Copy, Debug)]
698#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
699pub enum EventItemOrigin {
700    /// The event was created locally.
701    Local,
702    /// The event came from a sync response.
703    Sync,
704    /// The event came from pagination.
705    Pagination,
706    /// The event came from a cache.
707    Cache,
708}
709
710/// What's the status of a reaction?
711#[derive(Clone, Debug)]
712pub enum ReactionStatus {
713    /// It's a local reaction to a local echo.
714    ///
715    /// The handle is missing only in testing contexts.
716    LocalToLocal(Option<SendReactionHandle>),
717    /// It's a local reaction to a remote event.
718    ///
719    /// The handle is missing only in testing contexts.
720    LocalToRemote(Option<SendHandle>),
721    /// It's a remote reaction to a remote event.
722    ///
723    /// The event id is that of the reaction event (not the target event).
724    RemoteToRemote(OwnedEventId),
725}
726
727/// Information about a single reaction stored in [`ReactionsByKeyBySender`].
728#[derive(Clone, Debug)]
729pub struct ReactionInfo {
730    pub timestamp: MilliSecondsSinceUnixEpoch,
731    /// Current status of this reaction.
732    pub status: ReactionStatus,
733}
734
735/// Reactions grouped by key first, then by sender.
736///
737/// This representation makes sure that a given sender has sent at most one
738/// reaction for an event.
739#[derive(Debug, Clone, Default)]
740pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
741
742impl Deref for ReactionsByKeyBySender {
743    type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
744
745    fn deref(&self) -> &Self::Target {
746        &self.0
747    }
748}
749
750impl DerefMut for ReactionsByKeyBySender {
751    fn deref_mut(&mut self) -> &mut Self::Target {
752        &mut self.0
753    }
754}
755
756impl ReactionsByKeyBySender {
757    /// Removes (in place) a reaction from the sender with the given annotation
758    /// from the mapping.
759    ///
760    /// Returns true if the reaction was found and thus removed, false
761    /// otherwise.
762    pub(crate) fn remove_reaction(
763        &mut self,
764        sender: &UserId,
765        annotation: &str,
766    ) -> Option<ReactionInfo> {
767        if let Some(by_user) = self.0.get_mut(annotation) {
768            if let Some(info) = by_user.swap_remove(sender) {
769                // If this was the last reaction, remove the annotation entry.
770                if by_user.is_empty() {
771                    self.0.swap_remove(annotation);
772                }
773                return Some(info);
774            }
775        }
776        None
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use assert_matches::assert_matches;
783    use assert_matches2::assert_let;
784    use matrix_sdk::test_utils::logged_in_client;
785    use matrix_sdk_base::{
786        deserialized_responses::TimelineEvent, latest_event::LatestEvent, MinimalStateEvent,
787        OriginalMinimalStateEvent, RequestedRequiredStates,
788    };
789    use matrix_sdk_test::{
790        async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event,
791    };
792    use ruma::{
793        api::client::sync::sync_events::v5 as http,
794        event_id,
795        events::{
796            room::{
797                member::RoomMemberEventContent,
798                message::{MessageFormat, MessageType},
799            },
800            AnySyncStateEvent, AnySyncTimelineEvent, BundledMessageLikeRelations,
801        },
802        room_id,
803        serde::Raw,
804        user_id, RoomId, UInt, UserId,
805    };
806
807    use super::{EventTimelineItem, Profile};
808    use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
809
810    #[async_test]
811    async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
812        // Given a sync event that is suitable to be used as a latest_event
813
814        let room_id = room_id!("!q:x.uk");
815        let user_id = user_id!("@t:o.uk");
816        let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
817        let client = logged_in_client(None).await;
818
819        // When we construct a timeline event from it
820        let timeline_item =
821            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
822                .await
823                .unwrap();
824
825        // Then its properties correctly translate
826        assert_eq!(timeline_item.sender, user_id);
827        assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
828        assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
829        if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
830            assert_eq!(txt.body, "**My M**");
831            let formatted = txt.formatted.as_ref().unwrap();
832            assert_eq!(formatted.format, MessageFormat::Html);
833            assert_eq!(formatted.body, "<b>My M</b>");
834        } else {
835            panic!("Unexpected message type");
836        }
837    }
838
839    #[async_test]
840    async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
841        // Given a sync knock member state event that is suitable to be used as a
842        // latest_event
843
844        let room_id = room_id!("!q:x.uk");
845        let user_id = user_id!("@t:o.uk");
846        let raw_event = member_event_as_state_event(
847            room_id,
848            user_id,
849            "knock",
850            "Alice Margatroid",
851            "mxc://e.org/SEs",
852        );
853        let client = logged_in_client(None).await;
854
855        // Add power levels state event, otherwise the knock state event can't be used
856        // as the latest event
857        let power_level_event = sync_state_event!({
858            "type": "m.room.power_levels",
859            "content": {},
860            "event_id": "$143278582443PhrSn:example.org",
861            "origin_server_ts": 143273581,
862            "room_id": room_id,
863            "sender": user_id,
864            "state_key": "",
865            "unsigned": {
866              "age": 1234
867            }
868        });
869        let mut room = http::response::Room::new();
870        room.required_state.push(power_level_event);
871
872        // And the room is stored in the client so it can be extracted when needed
873        let response = response_with_room(room_id, room);
874        client
875            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
876            .await
877            .unwrap();
878
879        // When we construct a timeline event from it
880        let event = TimelineEvent::new(raw_event.cast());
881        let timeline_item =
882            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
883                .await
884                .unwrap();
885
886        // Then its properties correctly translate
887        assert_eq!(timeline_item.sender, user_id);
888        assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
889        assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
890        if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
891            assert_eq!(change.user_id, user_id);
892            assert_matches!(change.change, Some(MembershipChange::Knocked));
893        } else {
894            panic!("Unexpected state event type");
895        }
896    }
897
898    #[async_test]
899    async fn test_latest_message_includes_bundled_edit() {
900        // Given a sync event that is suitable to be used as a latest_event, and
901        // contains a bundled edit,
902        let room_id = room_id!("!q:x.uk");
903        let user_id = user_id!("@t:o.uk");
904
905        let f = EventFactory::new();
906
907        let original_event_id = event_id!("$original");
908
909        let mut relations = BundledMessageLikeRelations::new();
910        relations.replace = Some(Box::new(
911            f.text_html(" * Updated!", " * <b>Updated!</b>")
912                .edit(
913                    original_event_id,
914                    MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
915                )
916                .event_id(event_id!("$edit"))
917                .sender(user_id)
918                .into_raw_sync(),
919        ));
920
921        let event = f
922            .text_html("**My M**", "<b>My M</b>")
923            .sender(user_id)
924            .event_id(original_event_id)
925            .bundled_relations(relations)
926            .server_ts(42)
927            .into_event();
928
929        let client = logged_in_client(None).await;
930
931        // When we construct a timeline event from it,
932        let timeline_item =
933            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
934                .await
935                .unwrap();
936
937        // Then its properties correctly translate.
938        assert_eq!(timeline_item.sender, user_id);
939        assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
940        assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
941        if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
942            assert_eq!(txt.body, "Updated!");
943            let formatted = txt.formatted.as_ref().unwrap();
944            assert_eq!(formatted.format, MessageFormat::Html);
945            assert_eq!(formatted.body, "<b>Updated!</b>");
946        } else {
947            panic!("Unexpected message type");
948        }
949    }
950
951    #[async_test]
952    async fn test_latest_poll_includes_bundled_edit() {
953        // Given a sync event that is suitable to be used as a latest_event, and
954        // contains a bundled edit,
955        let room_id = room_id!("!q:x.uk");
956        let user_id = user_id!("@t:o.uk");
957
958        let f = EventFactory::new();
959
960        let original_event_id = event_id!("$original");
961
962        let mut relations = BundledMessageLikeRelations::new();
963        relations.replace = Some(Box::new(
964            f.poll_edit(
965                original_event_id,
966                "It's one banana, Michael, how much could it cost?",
967                vec!["1 dollar", "10 dollars", "100 dollars"],
968            )
969            .event_id(event_id!("$edit"))
970            .sender(user_id)
971            .into_raw_sync(),
972        ));
973
974        let event = f
975            .poll_start(
976                "It's one avocado, Michael, how much could it cost? 10 dollars?",
977                "It's one avocado, Michael, how much could it cost?",
978                vec!["1 dollar", "10 dollars", "100 dollars"],
979            )
980            .event_id(original_event_id)
981            .bundled_relations(relations)
982            .sender(user_id)
983            .into_event();
984
985        let client = logged_in_client(None).await;
986
987        // When we construct a timeline event from it,
988        let timeline_item =
989            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
990                .await
991                .unwrap();
992
993        // Then its properties correctly translate.
994        assert_eq!(timeline_item.sender, user_id);
995
996        let poll = timeline_item.content().as_poll().unwrap();
997        assert!(poll.has_been_edited);
998        assert_eq!(
999            poll.start_event_content.poll_start.question.text,
1000            "It's one banana, Michael, how much could it cost?"
1001        );
1002    }
1003
1004    #[async_test]
1005    async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
1006    ) {
1007        // Given a sync event that is suitable to be used as a latest_event, and a room
1008        // with a member event for the sender
1009
1010        use ruma::owned_mxc_uri;
1011        let room_id = room_id!("!q:x.uk");
1012        let user_id = user_id!("@t:o.uk");
1013        let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1014        let client = logged_in_client(None).await;
1015        let mut room = http::response::Room::new();
1016        room.required_state.push(member_event_as_state_event(
1017            room_id,
1018            user_id,
1019            "join",
1020            "Alice Margatroid",
1021            "mxc://e.org/SEs",
1022        ));
1023
1024        // And the room is stored in the client so it can be extracted when needed
1025        let response = response_with_room(room_id, room);
1026        client
1027            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1028            .await
1029            .unwrap();
1030
1031        // When we construct a timeline event from it
1032        let timeline_item =
1033            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1034                .await
1035                .unwrap();
1036
1037        // Then its sender is properly populated
1038        assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1039        assert_eq!(
1040            profile,
1041            Profile {
1042                display_name: Some("Alice Margatroid".to_owned()),
1043                display_name_ambiguous: false,
1044                avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1045            }
1046        );
1047    }
1048
1049    #[async_test]
1050    async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
1051    ) {
1052        // Given a sync event that is suitable to be used as a latest_event, a room, and
1053        // a member event for the sender (which isn't part of the room yet).
1054
1055        use ruma::owned_mxc_uri;
1056        let room_id = room_id!("!q:x.uk");
1057        let user_id = user_id!("@t:o.uk");
1058        let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1059        let client = logged_in_client(None).await;
1060
1061        let member_event = MinimalStateEvent::Original(
1062            member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")
1063                .deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
1064                .unwrap(),
1065        );
1066
1067        let room = http::response::Room::new();
1068        // Do not push the `member_event` inside the room. Let's say it's flying in the
1069        // `StateChanges`.
1070
1071        // And the room is stored in the client so it can be extracted when needed
1072        let response = response_with_room(room_id, room);
1073        client
1074            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1075            .await
1076            .unwrap();
1077
1078        // When we construct a timeline event from it
1079        let timeline_item = EventTimelineItem::from_latest_event(
1080            client,
1081            room_id,
1082            LatestEvent::new_with_sender_details(event, Some(member_event), None),
1083        )
1084        .await
1085        .unwrap();
1086
1087        // Then its sender is properly populated
1088        assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1089        assert_eq!(
1090            profile,
1091            Profile {
1092                display_name: Some("Alice Margatroid".to_owned()),
1093                display_name_ambiguous: false,
1094                avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1095            }
1096        );
1097    }
1098
1099    #[async_test]
1100    async fn test_emoji_detection() {
1101        let room_id = room_id!("!q:x.uk");
1102        let user_id = user_id!("@t:o.uk");
1103        let client = logged_in_client(None).await;
1104
1105        let mut event = message_event(room_id, user_id, "🤷‍♂️ No boost 🤷‍♂️", "", 0);
1106        let mut timeline_item =
1107            EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1108                .await
1109                .unwrap();
1110
1111        assert!(!timeline_item.contains_only_emojis());
1112
1113        // Ignores leading and trailing white spaces
1114        event = message_event(room_id, user_id, " 🚀 ", "", 0);
1115        timeline_item =
1116            EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1117                .await
1118                .unwrap();
1119
1120        assert!(timeline_item.contains_only_emojis());
1121
1122        // Too many
1123        event = message_event(room_id, user_id, "👨‍👩‍👦1️⃣🚀👳🏾‍♂️🪩👍👍🏻🫱🏼‍🫲🏾🙂👋", "", 0);
1124        timeline_item =
1125            EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1126                .await
1127                .unwrap();
1128
1129        assert!(!timeline_item.contains_only_emojis());
1130
1131        // Works with combined emojis
1132        event = message_event(room_id, user_id, "👨‍👩‍👦1️⃣👳🏾‍♂️👍🏻🫱🏼‍🫲🏾", "", 0);
1133        timeline_item =
1134            EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1135                .await
1136                .unwrap();
1137
1138        assert!(timeline_item.contains_only_emojis());
1139    }
1140
1141    fn member_event(
1142        room_id: &RoomId,
1143        user_id: &UserId,
1144        display_name: &str,
1145        avatar_url: &str,
1146    ) -> Raw<AnySyncTimelineEvent> {
1147        sync_timeline_event!({
1148            "type": "m.room.member",
1149            "content": {
1150                "avatar_url": avatar_url,
1151                "displayname": display_name,
1152                "membership": "join",
1153                "reason": ""
1154            },
1155            "event_id": "$143273582443PhrSn:example.org",
1156            "origin_server_ts": 143273583,
1157            "room_id": room_id,
1158            "sender": "@example:example.org",
1159            "state_key": user_id,
1160            "type": "m.room.member",
1161            "unsigned": {
1162              "age": 1234
1163            }
1164        })
1165    }
1166
1167    fn member_event_as_state_event(
1168        room_id: &RoomId,
1169        user_id: &UserId,
1170        membership: &str,
1171        display_name: &str,
1172        avatar_url: &str,
1173    ) -> Raw<AnySyncStateEvent> {
1174        sync_state_event!({
1175            "type": "m.room.member",
1176            "content": {
1177                "avatar_url": avatar_url,
1178                "displayname": display_name,
1179                "membership": membership,
1180                "reason": ""
1181            },
1182            "event_id": "$143273582443PhrSn:example.org",
1183            "origin_server_ts": 143273583,
1184            "room_id": room_id,
1185            "sender": user_id,
1186            "state_key": user_id,
1187            "unsigned": {
1188              "age": 1234
1189            }
1190        })
1191    }
1192
1193    fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
1194        let mut response = http::Response::new("6".to_owned());
1195        response.rooms.insert(room_id.to_owned(), room);
1196        response
1197    }
1198
1199    fn message_event(
1200        room_id: &RoomId,
1201        user_id: &UserId,
1202        body: &str,
1203        formatted_body: &str,
1204        ts: u64,
1205    ) -> TimelineEvent {
1206        TimelineEvent::new(sync_timeline_event!({
1207            "event_id": "$eventid6",
1208            "sender": user_id,
1209            "origin_server_ts": ts,
1210            "type": "m.room.message",
1211            "room_id": room_id.to_string(),
1212            "content": {
1213                "body": body,
1214                "format": "org.matrix.custom.html",
1215                "formatted_body": formatted_body,
1216                "msgtype": "m.text"
1217            },
1218        }))
1219    }
1220}