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