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    Error,
24    deserialized_responses::{EncryptionInfo, ShieldState},
25    send_queue::{SendHandle, SendReactionHandle},
26};
27use matrix_sdk_base::deserialized_responses::{SENT_IN_CLEAR, ShieldStateCode};
28use once_cell::sync::Lazy;
29use ruma::{
30    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
31    OwnedUserId, TransactionId, UserId,
32    events::{AnySyncTimelineEvent, receipt::Receipt, room::message::MessageType},
33    room_version_rules::RedactionRules,
34    serde::Raw,
35};
36use unicode_segmentation::UnicodeSegmentation;
37
38mod content;
39mod local;
40mod remote;
41
42pub use self::{
43    content::{
44        AnyOtherFullStateEventContent, EmbeddedEvent, EncryptedMessage, InReplyToDetails,
45        MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind,
46        OtherMessageLike, OtherState, PollResult, PollState, RoomMembershipChange,
47        RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
48    },
49    local::{EventSendState, MediaUploadProgress},
50};
51pub(super) use self::{
52    content::{
53        extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
54    },
55    local::LocalEventTimelineItem,
56    remote::{RemoteEventOrigin, RemoteEventTimelineItem},
57};
58
59/// An item in the timeline that represents at least one event.
60///
61/// There is always one main event that gives the `EventTimelineItem` its
62/// identity but in many cases, additional events like reactions and edits are
63/// also part of the item.
64#[derive(Clone, Debug)]
65pub struct EventTimelineItem {
66    /// The sender of the event.
67    pub(super) sender: OwnedUserId,
68    /// The sender's profile of the event.
69    pub(super) sender_profile: TimelineDetails<Profile>,
70    /// The timestamp of the event.
71    pub(super) timestamp: MilliSecondsSinceUnixEpoch,
72    /// The content of the event.
73    pub(super) content: TimelineItemContent,
74    /// The kind of event timeline item, local or remote.
75    pub(super) kind: EventTimelineItemKind,
76    /// Whether or not the event belongs to an encrypted room.
77    ///
78    /// May be false when we don't know about the room encryption status yet.
79    pub(super) is_room_encrypted: bool,
80}
81
82#[derive(Clone, Debug)]
83pub(super) enum EventTimelineItemKind {
84    /// A local event, not yet echoed back by the server.
85    Local(LocalEventTimelineItem),
86    /// An event received from the server.
87    Remote(RemoteEventTimelineItem),
88}
89
90/// A wrapper that can contain either a transaction id, or an event id.
91#[derive(Clone, Debug, Eq, Hash, PartialEq)]
92pub enum TimelineEventItemId {
93    /// The item is local, identified by its transaction id (to be used in
94    /// subsequent requests).
95    TransactionId(OwnedTransactionId),
96    /// The item is remote, identified by its event id.
97    EventId(OwnedEventId),
98}
99
100/// An handle that usually allows to perform an action on a timeline event.
101///
102/// If the item represents a remote item, then the event id is usually
103/// sufficient to perform an action on it. Otherwise, the send queue handle is
104/// returned, if available.
105pub(crate) enum TimelineItemHandle<'a> {
106    Remote(&'a EventId),
107    Local(&'a SendHandle),
108}
109
110impl EventTimelineItem {
111    pub(super) fn new(
112        sender: OwnedUserId,
113        sender_profile: TimelineDetails<Profile>,
114        timestamp: MilliSecondsSinceUnixEpoch,
115        content: TimelineItemContent,
116        kind: EventTimelineItemKind,
117        is_room_encrypted: bool,
118    ) -> Self {
119        Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted }
120    }
121
122    /// Check whether this item is a local echo.
123    ///
124    /// This returns `true` for events created locally, until the server echoes
125    /// back the full event as part of a sync response.
126    ///
127    /// This is the opposite of [`Self::is_remote_event`].
128    pub fn is_local_echo(&self) -> bool {
129        matches!(self.kind, EventTimelineItemKind::Local(_))
130    }
131
132    /// Check whether this item is a remote event.
133    ///
134    /// This returns `true` only for events that have been echoed back from the
135    /// homeserver. A local echo sent but not echoed back yet will return
136    /// `false` here.
137    ///
138    /// This is the opposite of [`Self::is_local_echo`].
139    pub fn is_remote_event(&self) -> bool {
140        matches!(self.kind, EventTimelineItemKind::Remote(_))
141    }
142
143    /// Get the `LocalEventTimelineItem` if `self` is `Local`.
144    pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
145        as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
146    }
147
148    /// Get a reference to a [`RemoteEventTimelineItem`] if it's a remote echo.
149    pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
150        as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
151    }
152
153    /// Get a mutable reference to a [`RemoteEventTimelineItem`] if it's a
154    /// remote echo.
155    pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
156        as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
157    }
158
159    /// Get the event's send state of a local echo.
160    pub fn send_state(&self) -> Option<&EventSendState> {
161        as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
162    }
163
164    /// Get the time that the local event was pushed in the send queue at.
165    pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
166        match &self.kind {
167            EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
168            EventTimelineItemKind::Remote(_) => None,
169        }
170    }
171
172    /// Get the unique identifier of this item.
173    ///
174    /// Returns the transaction ID for a local echo item that has not been sent
175    /// and the event ID for a local echo item that has been sent or a
176    /// remote item.
177    pub fn identifier(&self) -> TimelineEventItemId {
178        match &self.kind {
179            EventTimelineItemKind::Local(local) => local.identifier(),
180            EventTimelineItemKind::Remote(remote) => {
181                TimelineEventItemId::EventId(remote.event_id.clone())
182            }
183        }
184    }
185
186    /// Get the transaction ID of a local echo item.
187    ///
188    /// The transaction ID is currently only kept until the remote echo for a
189    /// local event is received.
190    pub fn transaction_id(&self) -> Option<&TransactionId> {
191        as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
192    }
193
194    /// Get the event ID of this item.
195    ///
196    /// If this returns `Some(_)`, the event was successfully created by the
197    /// server.
198    ///
199    /// Even if this is a local event, this can be `Some(_)` as the event ID can
200    /// be known not just from the remote echo via `sync_events`, but also
201    /// from the response of the send request that created the event.
202    pub fn event_id(&self) -> Option<&EventId> {
203        match &self.kind {
204            EventTimelineItemKind::Local(local_event) => local_event.event_id(),
205            EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
206        }
207    }
208
209    /// Get the sender of this item.
210    pub fn sender(&self) -> &UserId {
211        &self.sender
212    }
213
214    /// Get the profile of the sender.
215    pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
216        &self.sender_profile
217    }
218
219    /// Get the content of this item.
220    pub fn content(&self) -> &TimelineItemContent {
221        &self.content
222    }
223
224    /// Get a mutable handle to the content of this item.
225    pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
226        &mut self.content
227    }
228
229    /// Get the read receipts of this item.
230    ///
231    /// The key is the ID of a room member and the value are details about the
232    /// read receipt.
233    ///
234    /// Note that currently this ignores threads.
235    pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
236        static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
237        match &self.kind {
238            EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
239            EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
240        }
241    }
242
243    /// Get the timestamp of this item.
244    ///
245    /// If this event hasn't been echoed back by the server yet, returns the
246    /// time the local event was created. Otherwise, returns the origin
247    /// server timestamp.
248    pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
249        self.timestamp
250    }
251
252    /// Whether this timeline item was sent by the logged-in user themselves.
253    pub fn is_own(&self) -> bool {
254        match &self.kind {
255            EventTimelineItemKind::Local(_) => true,
256            EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
257        }
258    }
259
260    /// Flag indicating this timeline item can be edited by the current user.
261    pub fn is_editable(&self) -> bool {
262        // Steps here should be in sync with [`EventTimelineItem::edit_info`] and
263        // [`Timeline::edit_poll`].
264
265        if !self.is_own() {
266            // In theory could work, but it's hard to compute locally.
267            return false;
268        }
269
270        match self.content() {
271            TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
272                MsgLikeKind::Message(message) => match message.msgtype() {
273                    MessageType::Text(_)
274                    | MessageType::Emote(_)
275                    | MessageType::Audio(_)
276                    | MessageType::File(_)
277                    | MessageType::Image(_)
278                    | MessageType::Video(_) => true,
279                    #[cfg(feature = "unstable-msc4274")]
280                    MessageType::Gallery(_) => true,
281                    _ => false,
282                },
283                MsgLikeKind::Poll(poll) => {
284                    poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
285                }
286                // Other MsgLike timeline items can't be edited at the moment.
287                _ => false,
288            },
289            _ => {
290                // Other timeline items can't be edited at the moment.
291                false
292            }
293        }
294    }
295
296    /// Whether the event should be highlighted in the timeline.
297    pub fn is_highlighted(&self) -> bool {
298        match &self.kind {
299            EventTimelineItemKind::Local(_) => false,
300            EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
301        }
302    }
303
304    /// Get the encryption information for the event, if any.
305    pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
306        match &self.kind {
307            EventTimelineItemKind::Local(_) => None,
308            EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
309        }
310    }
311
312    /// Gets the [`ShieldState`] which can be used to decorate messages in the
313    /// recommended way.
314    pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
315        if !self.is_room_encrypted || self.is_local_echo() {
316            return None;
317        }
318
319        // An unable-to-decrypt message has no authenticity shield.
320        if self.content().is_unable_to_decrypt() {
321            return None;
322        }
323
324        match self.encryption_info() {
325            Some(info) => {
326                if strict {
327                    Some(info.verification_state.to_shield_state_strict())
328                } else {
329                    Some(info.verification_state.to_shield_state_lax())
330                }
331            }
332            None => Some(ShieldState::Red {
333                code: ShieldStateCode::SentInClear,
334                message: SENT_IN_CLEAR,
335            }),
336        }
337    }
338
339    /// Check whether this item can be replied to.
340    pub fn can_be_replied_to(&self) -> bool {
341        // This must be in sync with the early returns of `Timeline::send_reply`
342        if self.event_id().is_none() {
343            false
344        } else if self.content.is_message() {
345            true
346        } else {
347            self.latest_json().is_some()
348        }
349    }
350
351    /// Get the raw JSON representation of the initial event (the one that
352    /// caused this timeline item to be created).
353    ///
354    /// Returns `None` if this event hasn't been echoed back by the server
355    /// yet.
356    pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
357        match &self.kind {
358            EventTimelineItemKind::Local(_) => None,
359            EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
360        }
361    }
362
363    /// Get the raw JSON representation of the latest edit, if any.
364    pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
365        match &self.kind {
366            EventTimelineItemKind::Local(_) => None,
367            EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
368        }
369    }
370
371    /// Shorthand for
372    /// `item.latest_edit_json().or_else(|| item.original_json())`.
373    pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
374        self.latest_edit_json().or_else(|| self.original_json())
375    }
376
377    /// Get the origin of the event, i.e. where it came from.
378    ///
379    /// May return `None` in some edge cases that are subject to change.
380    pub fn origin(&self) -> Option<EventItemOrigin> {
381        match &self.kind {
382            EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
383            EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
384                RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
385                RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
386                RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
387                RemoteEventOrigin::Unknown => None,
388            },
389        }
390    }
391
392    pub(super) fn set_content(&mut self, content: TimelineItemContent) {
393        self.content = content;
394    }
395
396    /// Clone the current event item, and update its `kind`.
397    pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
398        Self { kind: kind.into(), ..self.clone() }
399    }
400
401    /// Clone the current event item, and update its content.
402    pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
403        let mut new = self.clone();
404        new.content = new_content;
405        new
406    }
407
408    /// Clone the current event item, and update its content.
409    ///
410    /// Optionally update `latest_edit_json` if the update is an edit received
411    /// from the server.
412    pub(super) fn with_content_and_latest_edit(
413        &self,
414        new_content: TimelineItemContent,
415        edit_json: Option<Raw<AnySyncTimelineEvent>>,
416    ) -> Self {
417        let mut new = self.clone();
418        new.content = new_content;
419        if let EventTimelineItemKind::Remote(r) = &mut new.kind {
420            r.latest_edit_json = edit_json;
421        }
422        new
423    }
424
425    /// Clone the current event item, and update its `sender_profile`.
426    pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
427        Self { sender_profile, ..self.clone() }
428    }
429
430    /// Clone the current event item, and update its `encryption_info`.
431    pub(super) fn with_encryption_info(
432        &self,
433        encryption_info: Option<Arc<EncryptionInfo>>,
434    ) -> Self {
435        let mut new = self.clone();
436        if let EventTimelineItemKind::Remote(r) = &mut new.kind {
437            r.encryption_info = encryption_info;
438        }
439
440        new
441    }
442
443    /// Create a clone of the current item, with content that's been redacted.
444    pub(super) fn redact(&self, rules: &RedactionRules) -> Self {
445        let content = self.content.redact(rules);
446        let kind = match &self.kind {
447            EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
448            EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
449        };
450        Self {
451            sender: self.sender.clone(),
452            sender_profile: self.sender_profile.clone(),
453            timestamp: self.timestamp,
454            content,
455            kind,
456            is_room_encrypted: self.is_room_encrypted,
457        }
458    }
459
460    pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
461        match &self.kind {
462            EventTimelineItemKind::Local(local) => {
463                if let Some(event_id) = local.event_id() {
464                    TimelineItemHandle::Remote(event_id)
465                } else {
466                    TimelineItemHandle::Local(
467                        // The send_handle must always be present, except in tests.
468                        local.send_handle.as_ref().expect("Unexpected missing send_handle"),
469                    )
470                }
471            }
472            EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
473        }
474    }
475
476    /// For local echoes, return the associated send handle.
477    pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
478        as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
479    }
480
481    /// Some clients may want to know if a particular text message or media
482    /// caption contains only emojis so that they can render them bigger for
483    /// added effect.
484    ///
485    /// This function provides that feature with the following
486    /// behavior/limitations:
487    /// - ignores leading and trailing white spaces
488    /// - fails texts bigger than 5 graphemes for performance reasons
489    /// - checks the body only for [`MessageType::Text`]
490    /// - only checks the caption for [`MessageType::Audio`],
491    ///   [`MessageType::File`], [`MessageType::Image`], and
492    ///   [`MessageType::Video`] if present
493    /// - all other message types will not match
494    ///
495    /// # Examples
496    /// # fn render_timeline_item(timeline_item: TimelineItem) {
497    /// if timeline_item.contains_only_emojis() {
498    ///     // e.g. increase the font size
499    /// }
500    /// # }
501    ///
502    /// See `test_emoji_detection` for more examples.
503    pub fn contains_only_emojis(&self) -> bool {
504        let body = match self.content() {
505            TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
506                MsgLikeKind::Message(message) => match &message.msgtype {
507                    MessageType::Text(text) => Some(text.body.as_str()),
508                    MessageType::Audio(audio) => audio.caption(),
509                    MessageType::File(file) => file.caption(),
510                    MessageType::Image(image) => image.caption(),
511                    MessageType::Video(video) => video.caption(),
512                    _ => None,
513                },
514                MsgLikeKind::Sticker(_)
515                | MsgLikeKind::Poll(_)
516                | MsgLikeKind::Redacted
517                | MsgLikeKind::UnableToDecrypt(_)
518                | MsgLikeKind::Other(_) => None,
519            },
520            TimelineItemContent::MembershipChange(_)
521            | TimelineItemContent::ProfileChange(_)
522            | TimelineItemContent::OtherState(_)
523            | TimelineItemContent::FailedToParseMessageLike { .. }
524            | TimelineItemContent::FailedToParseState { .. }
525            | TimelineItemContent::CallInvite
526            | TimelineItemContent::RtcNotification => None,
527        };
528
529        if let Some(body) = body {
530            // Collect the graphemes after trimming white spaces.
531            let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
532
533            // Limit the check to 5 graphemes for performance and security
534            // reasons. This will probably be used for every new message so we
535            // want it to be fast and we don't want to allow a DoS attack by
536            // sending a huge message.
537            if graphemes.len() > 5 {
538                return false;
539            }
540
541            graphemes.iter().all(|g| emojis::get(g).is_some())
542        } else {
543            false
544        }
545    }
546}
547
548impl From<LocalEventTimelineItem> for EventTimelineItemKind {
549    fn from(value: LocalEventTimelineItem) -> Self {
550        EventTimelineItemKind::Local(value)
551    }
552}
553
554impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
555    fn from(value: RemoteEventTimelineItem) -> Self {
556        EventTimelineItemKind::Remote(value)
557    }
558}
559
560/// The display name and avatar URL of a room member.
561#[derive(Clone, Debug, Default, PartialEq, Eq)]
562pub struct Profile {
563    /// The display name, if set.
564    pub display_name: Option<String>,
565
566    /// Whether the display name is ambiguous.
567    ///
568    /// Note that in rooms with lazy-loading enabled, this could be `false` even
569    /// though the display name is actually ambiguous if not all member events
570    /// have been seen yet.
571    pub display_name_ambiguous: bool,
572
573    /// The avatar URL, if set.
574    pub avatar_url: Option<OwnedMxcUri>,
575}
576
577/// Some details of an [`EventTimelineItem`] that may require server requests
578/// other than just the regular
579/// [`sync_events`][ruma::api::client::sync::sync_events].
580#[derive(Clone, Debug)]
581pub enum TimelineDetails<T> {
582    /// The details are not available yet, and have not been requested from the
583    /// server.
584    Unavailable,
585
586    /// The details are not available yet, but have been requested.
587    Pending,
588
589    /// The details are available.
590    Ready(T),
591
592    /// An error occurred when fetching the details.
593    Error(Arc<Error>),
594}
595
596impl<T> TimelineDetails<T> {
597    pub(crate) fn from_initial_value(value: Option<T>) -> Self {
598        match value {
599            Some(v) => Self::Ready(v),
600            None => Self::Unavailable,
601        }
602    }
603
604    pub fn is_unavailable(&self) -> bool {
605        matches!(self, Self::Unavailable)
606    }
607
608    pub fn is_ready(&self) -> bool {
609        matches!(self, Self::Ready(_))
610    }
611}
612
613/// Where this event came.
614#[derive(Clone, Copy, Debug)]
615#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
616pub enum EventItemOrigin {
617    /// The event was created locally.
618    Local,
619    /// The event came from a sync response.
620    Sync,
621    /// The event came from pagination.
622    Pagination,
623    /// The event came from a cache.
624    Cache,
625}
626
627/// What's the status of a reaction?
628#[derive(Clone, Debug)]
629pub enum ReactionStatus {
630    /// It's a local reaction to a local echo.
631    ///
632    /// The handle is missing only in testing contexts.
633    LocalToLocal(Option<SendReactionHandle>),
634    /// It's a local reaction to a remote event.
635    ///
636    /// The handle is missing only in testing contexts.
637    LocalToRemote(Option<SendHandle>),
638    /// It's a remote reaction to a remote event.
639    ///
640    /// The event id is that of the reaction event (not the target event).
641    RemoteToRemote(OwnedEventId),
642}
643
644/// Information about a single reaction stored in [`ReactionsByKeyBySender`].
645#[derive(Clone, Debug)]
646pub struct ReactionInfo {
647    pub timestamp: MilliSecondsSinceUnixEpoch,
648    /// Current status of this reaction.
649    pub status: ReactionStatus,
650}
651
652/// Reactions grouped by key first, then by sender.
653///
654/// This representation makes sure that a given sender has sent at most one
655/// reaction for an event.
656#[derive(Debug, Clone, Default)]
657pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
658
659impl Deref for ReactionsByKeyBySender {
660    type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
661
662    fn deref(&self) -> &Self::Target {
663        &self.0
664    }
665}
666
667impl DerefMut for ReactionsByKeyBySender {
668    fn deref_mut(&mut self) -> &mut Self::Target {
669        &mut self.0
670    }
671}
672
673impl ReactionsByKeyBySender {
674    /// Removes (in place) a reaction from the sender with the given annotation
675    /// from the mapping.
676    ///
677    /// Returns true if the reaction was found and thus removed, false
678    /// otherwise.
679    pub(crate) fn remove_reaction(
680        &mut self,
681        sender: &UserId,
682        annotation: &str,
683    ) -> Option<ReactionInfo> {
684        if let Some(by_user) = self.0.get_mut(annotation)
685            && let Some(info) = by_user.swap_remove(sender)
686        {
687            // If this was the last reaction, remove the annotation entry.
688            if by_user.is_empty() {
689                self.0.swap_remove(annotation);
690            }
691            return Some(info);
692        }
693        None
694    }
695}