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