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 let TimelineItemContent::UnableToDecrypt(_) = self.content() {
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(_) | MsgLikeKind::Poll(_) => None,
599            },
600            TimelineItemContent::RedactedMessage
601            | TimelineItemContent::UnableToDecrypt(_)
602            | TimelineItemContent::MembershipChange(_)
603            | TimelineItemContent::ProfileChange(_)
604            | TimelineItemContent::OtherState(_)
605            | TimelineItemContent::FailedToParseMessageLike { .. }
606            | TimelineItemContent::FailedToParseState { .. }
607            | TimelineItemContent::CallInvite
608            | TimelineItemContent::CallNotify => None,
609        };
610
611        if let Some(body) = body {
612            // Collect the graphemes after trimming white spaces.
613            let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
614
615            // Limit the check to 5 graphemes for performance and security
616            // reasons. This will probably be used for every new message so we
617            // want it to be fast and we don't want to allow a DoS attack by
618            // sending a huge message.
619            if graphemes.len() > 5 {
620                return false;
621            }
622
623            graphemes.iter().all(|g| emojis::get(g).is_some())
624        } else {
625            false
626        }
627    }
628}
629
630impl From<LocalEventTimelineItem> for EventTimelineItemKind {
631    fn from(value: LocalEventTimelineItem) -> Self {
632        EventTimelineItemKind::Local(value)
633    }
634}
635
636impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
637    fn from(value: RemoteEventTimelineItem) -> Self {
638        EventTimelineItemKind::Remote(value)
639    }
640}
641
642/// The display name and avatar URL of a room member.
643#[derive(Clone, Debug, Default, PartialEq, Eq)]
644pub struct Profile {
645    /// The display name, if set.
646    pub display_name: Option<String>,
647
648    /// Whether the display name is ambiguous.
649    ///
650    /// Note that in rooms with lazy-loading enabled, this could be `false` even
651    /// though the display name is actually ambiguous if not all member events
652    /// have been seen yet.
653    pub display_name_ambiguous: bool,
654
655    /// The avatar URL, if set.
656    pub avatar_url: Option<OwnedMxcUri>,
657}
658
659/// Some details of an [`EventTimelineItem`] that may require server requests
660/// other than just the regular
661/// [`sync_events`][ruma::api::client::sync::sync_events].
662#[derive(Clone, Debug)]
663pub enum TimelineDetails<T> {
664    /// The details are not available yet, and have not been requested from the
665    /// server.
666    Unavailable,
667
668    /// The details are not available yet, but have been requested.
669    Pending,
670
671    /// The details are available.
672    Ready(T),
673
674    /// An error occurred when fetching the details.
675    Error(Arc<Error>),
676}
677
678impl<T> TimelineDetails<T> {
679    pub(crate) fn from_initial_value(value: Option<T>) -> Self {
680        match value {
681            Some(v) => Self::Ready(v),
682            None => Self::Unavailable,
683        }
684    }
685
686    pub(crate) fn is_unavailable(&self) -> bool {
687        matches!(self, Self::Unavailable)
688    }
689
690    pub fn is_ready(&self) -> bool {
691        matches!(self, Self::Ready(_))
692    }
693}
694
695/// Where this event came.
696#[derive(Clone, Copy, Debug)]
697#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
698pub enum EventItemOrigin {
699    /// The event was created locally.
700    Local,
701    /// The event came from a sync response.
702    Sync,
703    /// The event came from pagination.
704    Pagination,
705    /// The event came from a cache.
706    Cache,
707}
708
709/// What's the status of a reaction?
710#[derive(Clone, Debug)]
711pub enum ReactionStatus {
712    /// It's a local reaction to a local echo.
713    ///
714    /// The handle is missing only in testing contexts.
715    LocalToLocal(Option<SendReactionHandle>),
716    /// It's a local reaction to a remote event.
717    ///
718    /// The handle is missing only in testing contexts.
719    LocalToRemote(Option<SendHandle>),
720    /// It's a remote reaction to a remote event.
721    ///
722    /// The event id is that of the reaction event (not the target event).
723    RemoteToRemote(OwnedEventId),
724}
725
726/// Information about a single reaction stored in [`ReactionsByKeyBySender`].
727#[derive(Clone, Debug)]
728pub struct ReactionInfo {
729    pub timestamp: MilliSecondsSinceUnixEpoch,
730    /// Current status of this reaction.
731    pub status: ReactionStatus,
732}
733
734/// Reactions grouped by key first, then by sender.
735///
736/// This representation makes sure that a given sender has sent at most one
737/// reaction for an event.
738#[derive(Debug, Clone, Default)]
739pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
740
741impl Deref for ReactionsByKeyBySender {
742    type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
743
744    fn deref(&self) -> &Self::Target {
745        &self.0
746    }
747}
748
749impl DerefMut for ReactionsByKeyBySender {
750    fn deref_mut(&mut self) -> &mut Self::Target {
751        &mut self.0
752    }
753}
754
755impl ReactionsByKeyBySender {
756    /// Removes (in place) a reaction from the sender with the given annotation
757    /// from the mapping.
758    ///
759    /// Returns true if the reaction was found and thus removed, false
760    /// otherwise.
761    pub(crate) fn remove_reaction(
762        &mut self,
763        sender: &UserId,
764        annotation: &str,
765    ) -> Option<ReactionInfo> {
766        if let Some(by_user) = self.0.get_mut(annotation) {
767            if let Some(info) = by_user.swap_remove(sender) {
768                // If this was the last reaction, remove the annotation entry.
769                if by_user.is_empty() {
770                    self.0.swap_remove(annotation);
771                }
772                return Some(info);
773            }
774        }
775        None
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use assert_matches::assert_matches;
782    use assert_matches2::assert_let;
783    use matrix_sdk::test_utils::logged_in_client;
784    use matrix_sdk_base::{
785        deserialized_responses::TimelineEvent, latest_event::LatestEvent, MinimalStateEvent,
786        OriginalMinimalStateEvent, RequestedRequiredStates,
787    };
788    use matrix_sdk_test::{
789        async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event,
790    };
791    use ruma::{
792        api::client::sync::sync_events::v5 as http,
793        event_id,
794        events::{
795            room::{
796                member::RoomMemberEventContent,
797                message::{MessageFormat, MessageType},
798            },
799            AnySyncStateEvent, AnySyncTimelineEvent, BundledMessageLikeRelations,
800        },
801        room_id,
802        serde::Raw,
803        user_id, RoomId, UInt, UserId,
804    };
805
806    use super::{EventTimelineItem, Profile};
807    use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
808
809    #[async_test]
810    async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
811        // Given a sync event that is suitable to be used as a latest_event
812
813        let room_id = room_id!("!q:x.uk");
814        let user_id = user_id!("@t:o.uk");
815        let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
816        let client = logged_in_client(None).await;
817
818        // When we construct a timeline event from it
819        let timeline_item =
820            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
821                .await
822                .unwrap();
823
824        // Then its properties correctly translate
825        assert_eq!(timeline_item.sender, user_id);
826        assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
827        assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
828        if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
829            assert_eq!(txt.body, "**My M**");
830            let formatted = txt.formatted.as_ref().unwrap();
831            assert_eq!(formatted.format, MessageFormat::Html);
832            assert_eq!(formatted.body, "<b>My M</b>");
833        } else {
834            panic!("Unexpected message type");
835        }
836    }
837
838    #[async_test]
839    async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
840        // Given a sync knock member state event that is suitable to be used as a
841        // latest_event
842
843        let room_id = room_id!("!q:x.uk");
844        let user_id = user_id!("@t:o.uk");
845        let raw_event = member_event_as_state_event(
846            room_id,
847            user_id,
848            "knock",
849            "Alice Margatroid",
850            "mxc://e.org/SEs",
851        );
852        let client = logged_in_client(None).await;
853
854        // Add power levels state event, otherwise the knock state event can't be used
855        // as the latest event
856        let power_level_event = sync_state_event!({
857            "type": "m.room.power_levels",
858            "content": {},
859            "event_id": "$143278582443PhrSn:example.org",
860            "origin_server_ts": 143273581,
861            "room_id": room_id,
862            "sender": user_id,
863            "state_key": "",
864            "unsigned": {
865              "age": 1234
866            }
867        });
868        let mut room = http::response::Room::new();
869        room.required_state.push(power_level_event);
870
871        // And the room is stored in the client so it can be extracted when needed
872        let response = response_with_room(room_id, room);
873        client
874            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
875            .await
876            .unwrap();
877
878        // When we construct a timeline event from it
879        let event = TimelineEvent::new(raw_event.cast());
880        let timeline_item =
881            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
882                .await
883                .unwrap();
884
885        // Then its properties correctly translate
886        assert_eq!(timeline_item.sender, user_id);
887        assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
888        assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
889        if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
890            assert_eq!(change.user_id, user_id);
891            assert_matches!(change.change, Some(MembershipChange::Knocked));
892        } else {
893            panic!("Unexpected state event type");
894        }
895    }
896
897    #[async_test]
898    async fn test_latest_message_includes_bundled_edit() {
899        // Given a sync event that is suitable to be used as a latest_event, and
900        // contains a bundled edit,
901        let room_id = room_id!("!q:x.uk");
902        let user_id = user_id!("@t:o.uk");
903
904        let f = EventFactory::new();
905
906        let original_event_id = event_id!("$original");
907
908        let mut relations = BundledMessageLikeRelations::new();
909        relations.replace = Some(Box::new(
910            f.text_html(" * Updated!", " * <b>Updated!</b>")
911                .edit(
912                    original_event_id,
913                    MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
914                )
915                .event_id(event_id!("$edit"))
916                .sender(user_id)
917                .into_raw_sync(),
918        ));
919
920        let event = f
921            .text_html("**My M**", "<b>My M</b>")
922            .sender(user_id)
923            .event_id(original_event_id)
924            .bundled_relations(relations)
925            .server_ts(42)
926            .into_event();
927
928        let client = logged_in_client(None).await;
929
930        // When we construct a timeline event from it,
931        let timeline_item =
932            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
933                .await
934                .unwrap();
935
936        // Then its properties correctly translate.
937        assert_eq!(timeline_item.sender, user_id);
938        assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
939        assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
940        if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
941            assert_eq!(txt.body, "Updated!");
942            let formatted = txt.formatted.as_ref().unwrap();
943            assert_eq!(formatted.format, MessageFormat::Html);
944            assert_eq!(formatted.body, "<b>Updated!</b>");
945        } else {
946            panic!("Unexpected message type");
947        }
948    }
949
950    #[async_test]
951    async fn test_latest_poll_includes_bundled_edit() {
952        // Given a sync event that is suitable to be used as a latest_event, and
953        // contains a bundled edit,
954        let room_id = room_id!("!q:x.uk");
955        let user_id = user_id!("@t:o.uk");
956
957        let f = EventFactory::new();
958
959        let original_event_id = event_id!("$original");
960
961        let mut relations = BundledMessageLikeRelations::new();
962        relations.replace = Some(Box::new(
963            f.poll_edit(
964                original_event_id,
965                "It's one banana, Michael, how much could it cost?",
966                vec!["1 dollar", "10 dollars", "100 dollars"],
967            )
968            .event_id(event_id!("$edit"))
969            .sender(user_id)
970            .into_raw_sync(),
971        ));
972
973        let event = f
974            .poll_start(
975                "It's one avocado, Michael, how much could it cost? 10 dollars?",
976                "It's one avocado, Michael, how much could it cost?",
977                vec!["1 dollar", "10 dollars", "100 dollars"],
978            )
979            .event_id(original_event_id)
980            .bundled_relations(relations)
981            .sender(user_id)
982            .into_event();
983
984        let client = logged_in_client(None).await;
985
986        // When we construct a timeline event from it,
987        let timeline_item =
988            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
989                .await
990                .unwrap();
991
992        // Then its properties correctly translate.
993        assert_eq!(timeline_item.sender, user_id);
994
995        let poll = timeline_item.content().as_poll().unwrap();
996        assert!(poll.has_been_edited);
997        assert_eq!(
998            poll.start_event_content.poll_start.question.text,
999            "It's one banana, Michael, how much could it cost?"
1000        );
1001    }
1002
1003    #[async_test]
1004    async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
1005    ) {
1006        // Given a sync event that is suitable to be used as a latest_event, and a room
1007        // with a member event for the sender
1008
1009        use ruma::owned_mxc_uri;
1010        let room_id = room_id!("!q:x.uk");
1011        let user_id = user_id!("@t:o.uk");
1012        let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1013        let client = logged_in_client(None).await;
1014        let mut room = http::response::Room::new();
1015        room.required_state.push(member_event_as_state_event(
1016            room_id,
1017            user_id,
1018            "join",
1019            "Alice Margatroid",
1020            "mxc://e.org/SEs",
1021        ));
1022
1023        // And the room is stored in the client so it can be extracted when needed
1024        let response = response_with_room(room_id, room);
1025        client
1026            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1027            .await
1028            .unwrap();
1029
1030        // When we construct a timeline event from it
1031        let timeline_item =
1032            EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
1033                .await
1034                .unwrap();
1035
1036        // Then its sender is properly populated
1037        assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1038        assert_eq!(
1039            profile,
1040            Profile {
1041                display_name: Some("Alice Margatroid".to_owned()),
1042                display_name_ambiguous: false,
1043                avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1044            }
1045        );
1046    }
1047
1048    #[async_test]
1049    async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
1050    ) {
1051        // Given a sync event that is suitable to be used as a latest_event, a room, and
1052        // a member event for the sender (which isn't part of the room yet).
1053
1054        use ruma::owned_mxc_uri;
1055        let room_id = room_id!("!q:x.uk");
1056        let user_id = user_id!("@t:o.uk");
1057        let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
1058        let client = logged_in_client(None).await;
1059
1060        let member_event = MinimalStateEvent::Original(
1061            member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")
1062                .deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
1063                .unwrap(),
1064        );
1065
1066        let room = http::response::Room::new();
1067        // Do not push the `member_event` inside the room. Let's say it's flying in the
1068        // `StateChanges`.
1069
1070        // And the room is stored in the client so it can be extracted when needed
1071        let response = response_with_room(room_id, room);
1072        client
1073            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1074            .await
1075            .unwrap();
1076
1077        // When we construct a timeline event from it
1078        let timeline_item = EventTimelineItem::from_latest_event(
1079            client,
1080            room_id,
1081            LatestEvent::new_with_sender_details(event, Some(member_event), None),
1082        )
1083        .await
1084        .unwrap();
1085
1086        // Then its sender is properly populated
1087        assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
1088        assert_eq!(
1089            profile,
1090            Profile {
1091                display_name: Some("Alice Margatroid".to_owned()),
1092                display_name_ambiguous: false,
1093                avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
1094            }
1095        );
1096    }
1097
1098    #[async_test]
1099    async fn test_emoji_detection() {
1100        let room_id = room_id!("!q:x.uk");
1101        let user_id = user_id!("@t:o.uk");
1102        let client = logged_in_client(None).await;
1103
1104        let mut event = message_event(room_id, user_id, "πŸ€·β€β™‚οΈ No boost πŸ€·β€β™‚οΈ", "", 0);
1105        let mut timeline_item =
1106            EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1107                .await
1108                .unwrap();
1109
1110        assert!(!timeline_item.contains_only_emojis());
1111
1112        // Ignores leading and trailing white spaces
1113        event = message_event(room_id, user_id, " πŸš€ ", "", 0);
1114        timeline_item =
1115            EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1116                .await
1117                .unwrap();
1118
1119        assert!(timeline_item.contains_only_emojis());
1120
1121        // Too many
1122        event = message_event(room_id, user_id, "πŸ‘¨β€πŸ‘©β€πŸ‘¦1οΈβƒ£πŸš€πŸ‘³πŸΎβ€β™‚οΈπŸͺ©πŸ‘πŸ‘πŸ»πŸ«±πŸΌβ€πŸ«²πŸΎπŸ™‚πŸ‘‹", "", 0);
1123        timeline_item =
1124            EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1125                .await
1126                .unwrap();
1127
1128        assert!(!timeline_item.contains_only_emojis());
1129
1130        // Works with combined emojis
1131        event = message_event(room_id, user_id, "πŸ‘¨β€πŸ‘©β€πŸ‘¦1οΈβƒ£πŸ‘³πŸΎβ€β™‚οΈπŸ‘πŸ»πŸ«±πŸΌβ€πŸ«²πŸΎ", "", 0);
1132        timeline_item =
1133            EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
1134                .await
1135                .unwrap();
1136
1137        assert!(timeline_item.contains_only_emojis());
1138    }
1139
1140    fn member_event(
1141        room_id: &RoomId,
1142        user_id: &UserId,
1143        display_name: &str,
1144        avatar_url: &str,
1145    ) -> Raw<AnySyncTimelineEvent> {
1146        sync_timeline_event!({
1147            "type": "m.room.member",
1148            "content": {
1149                "avatar_url": avatar_url,
1150                "displayname": display_name,
1151                "membership": "join",
1152                "reason": ""
1153            },
1154            "event_id": "$143273582443PhrSn:example.org",
1155            "origin_server_ts": 143273583,
1156            "room_id": room_id,
1157            "sender": "@example:example.org",
1158            "state_key": user_id,
1159            "type": "m.room.member",
1160            "unsigned": {
1161              "age": 1234
1162            }
1163        })
1164    }
1165
1166    fn member_event_as_state_event(
1167        room_id: &RoomId,
1168        user_id: &UserId,
1169        membership: &str,
1170        display_name: &str,
1171        avatar_url: &str,
1172    ) -> Raw<AnySyncStateEvent> {
1173        sync_state_event!({
1174            "type": "m.room.member",
1175            "content": {
1176                "avatar_url": avatar_url,
1177                "displayname": display_name,
1178                "membership": membership,
1179                "reason": ""
1180            },
1181            "event_id": "$143273582443PhrSn:example.org",
1182            "origin_server_ts": 143273583,
1183            "room_id": room_id,
1184            "sender": user_id,
1185            "state_key": user_id,
1186            "unsigned": {
1187              "age": 1234
1188            }
1189        })
1190    }
1191
1192    fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
1193        let mut response = http::Response::new("6".to_owned());
1194        response.rooms.insert(room_id.to_owned(), room);
1195        response
1196    }
1197
1198    fn message_event(
1199        room_id: &RoomId,
1200        user_id: &UserId,
1201        body: &str,
1202        formatted_body: &str,
1203        ts: u64,
1204    ) -> TimelineEvent {
1205        TimelineEvent::new(sync_timeline_event!({
1206            "event_id": "$eventid6",
1207            "sender": user_id,
1208            "origin_server_ts": ts,
1209            "type": "m.room.message",
1210            "room_id": room_id.to_string(),
1211            "content": {
1212                "body": body,
1213                "format": "org.matrix.custom.html",
1214                "formatted_body": formatted_body,
1215                "msgtype": "m.text"
1216            },
1217        }))
1218    }
1219}