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