matrix_sdk_ui/timeline/event_item/
mod.rs

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