Skip to main content

matrix_sdk_ui/timeline/
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
15//! A high-level view into a room's contents.
16//!
17//! See [`Timeline`] for details.
18
19use std::{fs, iter, path::PathBuf, sync::Arc};
20
21use algorithms::rfind_event_by_item_id;
22use event_item::TimelineItemHandle;
23use eyeball_im::VectorDiff;
24#[cfg(feature = "unstable-msc4274")]
25use futures::SendGallery;
26use futures_core::Stream;
27use imbl::Vector;
28use matrix_sdk::{
29    Result,
30    attachment::{AttachmentInfo, Thumbnail},
31    deserialized_responses::TimelineEvent,
32    event_cache::{EventCacheDropHandles, EventFocusThreadMode},
33    room::{
34        Receipts, Room,
35        edit::EditedContent,
36        reply::{EnforceThread, Reply},
37    },
38    send_queue::{RoomSendQueueError, SendHandle},
39    task_monitor::BackgroundTaskHandle,
40};
41use mime::Mime;
42use ruma::{
43    EventId, OwnedEventId, OwnedTransactionId, UserId,
44    api::client::receipt::create_receipt::v3::ReceiptType,
45    events::{
46        AnyMessageLikeEventContent, AnySyncTimelineEvent, Mentions,
47        location::{AssetType, LocationContent, ZoomLevel},
48        poll::unstable_start::{NewUnstablePollStartEventContent, UnstablePollStartEventContent},
49        receipt::{Receipt, ReceiptThread},
50        relation::{RelationType, Thread},
51        room::message::{
52            AddMentions, LocationMessageEventContent, MessageType, Relation,
53            RelationWithoutReplacement, ReplyWithinThread, RoomMessageEventContent,
54            RoomMessageEventContentWithoutRelation, TextMessageEventContent,
55        },
56    },
57    room_version_rules::RoomVersionRules,
58};
59use subscriber::TimelineWithDropHandle;
60use thiserror::Error;
61use tracing::{instrument, trace, warn};
62
63use self::{
64    algorithms::rfind_event_by_id, controller::TimelineController, futures::SendAttachment,
65};
66use crate::timeline::controller::{CryptoDropHandles, SendReceiptDecision};
67
68mod algorithms;
69mod builder;
70mod controller;
71mod date_dividers;
72mod error;
73pub mod event_filter;
74mod event_handler;
75mod event_item;
76pub mod futures;
77mod item;
78mod latest_event;
79mod pagination;
80mod subscriber;
81mod tasks;
82#[cfg(test)]
83mod tests;
84pub mod thread_list_service;
85mod traits;
86mod virtual_item;
87
88pub use self::{
89    builder::TimelineBuilder,
90    controller::default_event_filter,
91    error::*,
92    event_filter::{TimelineEventCondition, TimelineEventFilter},
93    event_item::{
94        AnyOtherStateEventContentChange, BeaconInfo, EditRevision, EmbeddedEvent, EncryptedMessage,
95        EventItemOrigin, EventSendState, EventTimelineItem, InReplyToDetails, LiveLocationState,
96        MediaUploadProgress, MemberProfileChange, MembershipChange, Message, MsgLikeContent,
97        MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo,
98        ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange, Sticker,
99        ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineEventShieldState,
100        TimelineEventShieldStateCode, TimelineItemContent,
101    },
102    item::{TimelineItem, TimelineItemKind, TimelineUniqueId},
103    latest_event::{LatestEventValue, LatestEventValueLocalState},
104    thread_list_service::{ThreadListPaginationState, ThreadListService},
105    traits::RoomExt,
106    virtual_item::VirtualTimelineItem,
107};
108
109/// Which pending send on an item [`Timeline::retry_send`] and
110/// [`Timeline::abort_send`] act on.
111#[derive(Clone, Debug)]
112pub enum SendTarget {
113    /// The item itself, while it's a local echo.
114    ///
115    /// Note that aborting one that's already in flight queues a redaction for
116    /// it, without a reason; use [`SendHandle::abort_with_reason`] if one is
117    /// needed.
118    ///
119    /// [`SendHandle::abort_with_reason`]: matrix_sdk::send_queue::SendHandle::abort_with_reason
120    Event,
121    /// Our pending edit of the item.
122    Edit,
123    /// Our pending redaction of the item.
124    Redaction,
125    /// Our pending reaction to the item with this key.
126    Reaction { key: String },
127}
128
129/// A high-level view into a regular¹ room's contents.
130///
131/// ¹ This type is meant to be used in the context of rooms without a
132/// `room_type`, that is rooms that are primarily used to exchange text
133/// messages.
134#[derive(Debug)]
135pub struct Timeline {
136    /// Cloneable, inner fields of the `Timeline`, shared with some background
137    /// tasks.
138    controller: TimelineController,
139
140    /// References to long-running tasks held by the timeline.
141    drop_handle: Arc<TimelineDropHandle>,
142}
143
144/// What should the timeline focus on?
145#[derive(Clone, Debug, PartialEq)]
146pub enum TimelineFocus {
147    /// Focus on live events, i.e. receive events from sync and append them in
148    /// real-time.
149    Live {
150        /// Whether to hide in-thread replies from the live timeline.
151        ///
152        /// This should be set to true when the client can create
153        /// [`Self::Thread`]-focused timelines from the thread roots themselves.
154        hide_threaded_events: bool,
155    },
156
157    /// Focus on a specific event, e.g. after clicking a permalink.
158    Event {
159        target: OwnedEventId,
160        num_context_events: u16,
161        /// How to handle threaded events.
162        thread_mode: TimelineEventFocusThreadMode,
163    },
164
165    /// Focus on a specific thread
166    Thread { thread_id: OwnedEventId },
167
168    /// Only show pinned events.
169    PinnedEvents,
170}
171
172/// Options for controlling the behaviour of [`TimelineFocus::Event`] for
173/// threaded events.
174#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
175#[derive(Clone, Copy, Debug, PartialEq)]
176pub enum TimelineEventFocusThreadMode {
177    /// Force the timeline into threaded mode.
178    ///
179    /// When the focused event is part of a thread, the timeline will be focused
180    /// on that thread's root. Otherwise, the timeline will treat the target
181    /// event itself as the thread root. Threaded events will never be hidden.
182    ForceThread,
183
184    /// Automatically determine if the target event is part of a thread or not.
185    ///
186    /// If the event is part of a thread, the timeline will be filtered to
187    /// on-thread events.
188    Automatic {
189        /// When the target event is not part of a thread, whether to hide
190        /// in-thread replies from the live timeline.
191        ///
192        /// Has no effect when the target event is part of a thread.
193        ///
194        /// This should be set to true when the client can create
195        /// [`TimelineFocus::Thread`]-focused timelines from the thread roots
196        /// themselves and doesn't use the [`Self::ForceThread`] mode.
197        hide_threaded_events: bool,
198    },
199}
200
201impl From<TimelineEventFocusThreadMode> for EventFocusThreadMode {
202    fn from(val: TimelineEventFocusThreadMode) -> Self {
203        match val {
204            TimelineEventFocusThreadMode::ForceThread => EventFocusThreadMode::ForceThread,
205            TimelineEventFocusThreadMode::Automatic { .. } => EventFocusThreadMode::Automatic,
206        }
207    }
208}
209
210impl TimelineFocus {
211    pub(super) fn debug_string(&self) -> String {
212        match self {
213            TimelineFocus::Live { .. } => "live".to_owned(),
214            TimelineFocus::Event { target, .. } => format!("permalink:{target}"),
215            TimelineFocus::Thread { thread_id, .. } => {
216                format!("thread:{thread_id}")
217            }
218            TimelineFocus::PinnedEvents => "pinned-events".to_owned(),
219        }
220    }
221}
222
223/// Changes how dividers get inserted, either in between each day or in between
224/// each month
225#[derive(Debug, Clone)]
226pub enum DateDividerMode {
227    Daily,
228    Monthly,
229}
230
231/// Configuration for sending an attachment.
232///
233/// Like [`matrix_sdk::attachment::AttachmentConfig`], but instead of the
234/// `reply` field, there's only a `in_reply_to` event id; it's the timeline
235/// deciding to fill the rest of the reply parameters.
236#[derive(Debug, Default)]
237pub struct AttachmentConfig {
238    pub txn_id: Option<OwnedTransactionId>,
239    pub info: Option<AttachmentInfo>,
240    pub thumbnail: Option<Thumbnail>,
241    pub caption: Option<TextMessageEventContent>,
242    pub mentions: Option<Mentions>,
243    pub in_reply_to: Option<OwnedEventId>,
244    pub extra_content: Option<serde_json::Map<String, serde_json::Value>>,
245}
246
247impl Timeline {
248    /// Returns the room for this timeline.
249    pub fn room(&self) -> &Room {
250        self.controller.room()
251    }
252
253    /// Clear all timeline items.
254    pub async fn clear(&self) {
255        self.controller.clear().await;
256    }
257
258    /// Retry decryption of previously un-decryptable events given a list of
259    /// session IDs whose keys have been imported.
260    ///
261    /// # Examples
262    ///
263    /// ```no_run
264    /// # use std::{path::PathBuf, time::Duration};
265    /// # use matrix_sdk::{Client, config::SyncSettings, ruma::room_id};
266    /// # use matrix_sdk_ui::Timeline;
267    /// # async {
268    /// # let mut client: Client = todo!();
269    /// # let room_id = ruma::room_id!("!example:example.org");
270    /// # let timeline: Timeline = todo!();
271    /// let path = PathBuf::from("/home/example/e2e-keys.txt");
272    /// let result =
273    ///     client.encryption().import_room_keys(path, "secret-passphrase").await?;
274    ///
275    /// // Given a timeline for a specific room_id
276    /// if let Some(keys_for_users) = result.keys.get(room_id) {
277    ///     let session_ids = keys_for_users.values().flatten();
278    ///     timeline.retry_decryption(session_ids).await;
279    /// }
280    /// # anyhow::Ok(()) };
281    /// ```
282    pub async fn retry_decryption<S: Into<String>>(
283        &self,
284        session_ids: impl IntoIterator<Item = S>,
285    ) {
286        self.controller
287            .retry_event_decryption(Some(session_ids.into_iter().map(Into::into).collect()))
288            .await;
289    }
290
291    #[tracing::instrument(skip(self))]
292    async fn retry_decryption_for_all_events(&self) {
293        self.controller.retry_event_decryption(None).await;
294    }
295
296    /// Get the current timeline item for the given event ID, if any.
297    ///
298    /// Will return a remote event, _or_ a local echo that has been sent but not
299    /// yet replaced by a remote echo.
300    ///
301    /// It's preferable to store the timeline items in the model for your UI, if
302    /// possible, instead of just storing IDs and coming back to the timeline
303    /// object to look up items.
304    pub async fn item_by_event_id(&self, event_id: &EventId) -> Option<EventTimelineItem> {
305        let items = self.controller.items().await;
306        let (_, item) = rfind_event_by_id(&items, event_id)?;
307        Some(item.to_owned())
308    }
309
310    /// Get the edit history for the given event.
311    ///
312    /// Returns all revisions of the event, in chronological order. The first
313    /// entry is the original event content, followed by each edit in the order
314    /// they were applied.
315    ///
316    /// This looks up the event and all `m.replace` relations targeting it,
317    /// first in the event cache and falling back to the homeserver if needed.
318    /// This works regardless of the timeline's focus kind (live, thread,
319    /// permalink, or pinned events).
320    pub async fn edit_revisions(&self, event_id: &EventId) -> Result<Vec<EditRevision>, Error> {
321        let Ok((original_event, edit_events)) = self
322            .controller
323            .find_event_with_relations(event_id, Some(vec![RelationType::Replacement]))
324            .await
325        else {
326            return Ok(Vec::new());
327        };
328
329        let room = self.room();
330        let mut revisions = Vec::with_capacity(edit_events.len() + 1);
331
332        for event in iter::once(original_event).chain(edit_events) {
333            let timestamp = event.timestamp();
334            if let Some(content) = TimelineItemContent::from_event(room, event).await {
335                revisions.push(EditRevision { content, timestamp });
336            }
337        }
338
339        Ok(revisions)
340    }
341
342    /// Get the latest of the timeline's remote event ids.
343    pub async fn latest_event_id(&self) -> Option<OwnedEventId> {
344        self.controller.latest_event_id().await
345    }
346
347    /// Get the current timeline items, along with a stream of updates of
348    /// timeline items.
349    ///
350    /// The stream produces `Vec<VectorDiff<_>>`, which means multiple updates
351    /// at once. There are no delays, it consumes as many updates as possible
352    /// and batches them.
353    pub async fn subscribe(
354        &self,
355    ) -> (Vector<Arc<TimelineItem>>, impl Stream<Item = Vec<VectorDiff<Arc<TimelineItem>>>> + use<>)
356    {
357        let (items, stream) = self.controller.subscribe().await;
358        let stream = TimelineWithDropHandle::new(stream, self.drop_handle.clone());
359        (items, stream)
360    }
361
362    /// Send a message to the room, and add it to the timeline as a local echo.
363    ///
364    /// For simplicity, this method doesn't currently allow custom message
365    /// types.
366    ///
367    /// If the encryption feature is enabled, this method will transparently
368    /// encrypt the room message if the room is encrypted.
369    ///
370    /// If sending the message fails, the local echo item will change its
371    /// `send_state` to [`EventSendState::SendingFailed`].
372    ///
373    /// This will do the right thing in the presence of threads:
374    ///
375    /// - if this timeline is not focused on a thread, then it will send the
376    ///   event as is.
377    /// - if this is a threaded timeline, and the event to send is a room
378    ///   message without a relationship, it will automatically mark it as a
379    ///   thread reply with the correct reply fallback, and send it.
380    ///
381    /// # Arguments
382    ///
383    /// - `content` - The content of the message event.
384    #[instrument(skip(self, content), fields(room_id = ?self.room().room_id()))]
385    pub async fn send(&self, content: AnyMessageLikeEventContent) -> Result<SendHandle, Error> {
386        self.send_with_extra_content(content, None).await
387    }
388
389    /// Queues an event in this room's send queue, with additional top-level
390    /// fields merged into its content. The event's own fields take precedence
391    /// on conflicts.
392    ///
393    /// See [`Self::send`] for more details.
394    #[instrument(skip(self, content, extra_content), fields(room_id = ?self.room().room_id()))]
395    pub async fn send_with_extra_content(
396        &self,
397        mut content: AnyMessageLikeEventContent,
398        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
399    ) -> Result<SendHandle, Error> {
400        // If this is a room event we're sending in a threaded timeline, we add
401        // the thread relation ourselves.
402        if content.relation().is_none()
403            && let Some(reply) = self.infer_reply(None).await
404        {
405            match &mut content {
406                AnyMessageLikeEventContent::RoomMessage(room_msg_content) => {
407                    content = self
408                        .room()
409                        .make_reply_event(
410                            // Note: this `.into()` gets rid of the relation,
411                            // but we've checked previously that the
412                            // `relates_to` field wasn't set.
413                            room_msg_content.clone().into(),
414                            reply,
415                        )
416                        .await?
417                        .into();
418                }
419
420                AnyMessageLikeEventContent::UnstablePollStart(
421                    UnstablePollStartEventContent::New(poll),
422                ) => {
423                    if let Some(thread_root) = self.controller.thread_root() {
424                        poll.relates_to = Some(RelationWithoutReplacement::Thread(Thread::plain(
425                            thread_root,
426                            reply.event_id,
427                        )));
428                    }
429                }
430
431                AnyMessageLikeEventContent::Sticker(sticker) => {
432                    if let Some(thread_root) = self.controller.thread_root() {
433                        sticker.relates_to =
434                            Some(Relation::Thread(Thread::plain(thread_root, reply.event_id)));
435                    }
436                }
437
438                _ => {}
439            }
440        }
441
442        let queue = self.room().send_queue();
443        let send = queue.send(content);
444        let send = match extra_content {
445            Some(extra_content) => send.with_extra_content(extra_content),
446            None => send,
447        };
448        Ok(send.await?)
449    }
450
451    /// Send a reply to the given event.
452    ///
453    /// Currently it only supports events with an event ID and JSON being
454    /// available (which can be removed by local redactions). This is subject to
455    /// change. Use [`EventTimelineItem::can_be_replied_to`] to decide whether
456    /// to render a reply button.
457    ///
458    /// The sender will be added to the mentions of the reply if and only if the
459    /// event has not been written by the sender.
460    ///
461    /// This will do the right thing in the presence of threads:
462    ///
463    /// - if this timeline is not focused on a thread, then it will forward the
464    ///   thread relationship of the replied-to event, if present.
465    /// - if this is a threaded timeline, it will mark the reply as an in-thread
466    ///   reply.
467    ///
468    /// # Arguments
469    ///
470    /// - `content` - The content of the reply.
471    /// - `in_reply_to` - The ID of the event to reply to.
472    #[instrument(skip(self, content))]
473    pub async fn send_reply(
474        &self,
475        content: RoomMessageEventContentWithoutRelation,
476        in_reply_to: OwnedEventId,
477    ) -> Result<SendHandle, Error> {
478        let reply = self
479            .infer_reply(Some(in_reply_to))
480            .await
481            .expect("the reply will always be set because we provided a replied-to event id");
482        let content = self.room().make_reply_event(content, reply).await?;
483        self.send(content.into()).await
484    }
485
486    /// Send a location event to the room, with `body` as the plain-text
487    /// fallback and `geo_uri` its RFC 5870 representation. With `in_reply_to`,
488    /// the location is sent as a reply, with [`Self::send_reply`] semantics.
489    #[instrument(skip(self, body, geo_uri, description))]
490    pub async fn send_location(
491        &self,
492        body: String,
493        geo_uri: String,
494        description: Option<String>,
495        zoom_level: Option<ZoomLevel>,
496        asset_type: Option<AssetType>,
497        in_reply_to: Option<OwnedEventId>,
498    ) -> Result<SendHandle, Error> {
499        let mut content = LocationMessageEventContent::new(body, geo_uri.clone());
500
501        if let Some(asset_type) = asset_type {
502            content = content.with_asset_type(asset_type);
503        }
504
505        let mut location = LocationContent::new(geo_uri);
506        location.description = description;
507        location.zoom_level = zoom_level;
508        content.location = Some(location);
509
510        let msgtype = MessageType::Location(content);
511
512        match in_reply_to {
513            Some(event_id) => {
514                self.send_reply(RoomMessageEventContentWithoutRelation::new(msgtype), event_id)
515                    .await
516            }
517            None => self.send(RoomMessageEventContent::new(msgtype).into()).await,
518        }
519    }
520
521    /// Given a message or media to send, and an optional `in_reply_to` event,
522    /// automatically fills the [`Reply`] information based on the current
523    /// timeline focus.
524    pub(crate) async fn infer_reply(&self, in_reply_to: Option<OwnedEventId>) -> Option<Reply> {
525        // If there's a replied-to event id, the reply is pretty
526        // straightforward, and we should only infer the `EnforceThread` based
527        // on the current focus.
528        if let Some(in_reply_to) = in_reply_to {
529            let enforce_thread = if self.controller.is_threaded() {
530                EnforceThread::Threaded(ReplyWithinThread::Yes)
531            } else {
532                EnforceThread::MaybeThreaded
533            };
534            return Some(Reply {
535                event_id: in_reply_to,
536                enforce_thread,
537                add_mentions: AddMentions::Yes,
538            });
539        }
540
541        let thread_root = self.controller.thread_root()?;
542
543        // The latest event id is used for the reply-to fallback, for clients
544        // which don't handle threads. It should be correctly set to the latest
545        // event in the thread, which the timeline instance might or might not
546        // know about; in this case, we do a best effort of filling it, and
547        // resort to using the thread root if we don't know about any event.
548        //
549        // Note: we could trigger a back-pagination if the timeline is empty,
550        // and wait for the results, if the timeline is too often empty.
551
552        let latest_event_id = self
553            .controller
554            .items()
555            .await
556            .iter()
557            .rev()
558            .find_map(|item| {
559                if let TimelineItemKind::Event(event) = item.kind() {
560                    event.event_id().map(ToOwned::to_owned)
561                } else {
562                    None
563                }
564            })
565            .unwrap_or(thread_root);
566
567        Some(Reply {
568            event_id: latest_event_id,
569            enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
570            add_mentions: AddMentions::Yes,
571        })
572    }
573
574    /// Edit an event given its [`TimelineEventItemId`] and some new content.
575    ///
576    /// Only supports events for which [`EventTimelineItem::is_editable()`]
577    /// returns `true`.
578    #[instrument(skip(self, new_content))]
579    pub async fn edit(
580        &self,
581        item_id: &TimelineEventItemId,
582        new_content: EditedContent,
583    ) -> Result<(), Error> {
584        let items = self.items().await;
585        let Some((_pos, item)) = rfind_event_by_item_id(&items, item_id) else {
586            return Err(Error::EventNotInTimeline(item_id.clone()));
587        };
588
589        match item.handle() {
590            TimelineItemHandle::Remote(event_id) => {
591                let content = self
592                    .room()
593                    .make_edit_event(event_id, new_content)
594                    .await
595                    .map_err(EditError::RoomError)?;
596                self.send(content).await?;
597                Ok(())
598            }
599
600            TimelineItemHandle::Local(handle) => {
601                let new_content: AnyMessageLikeEventContent = match new_content {
602                    EditedContent::RoomMessage(message) => {
603                        if item.content.is_message() {
604                            // The replacement becomes the pending event itself,
605                            // so restore its relations, which the payload can't
606                            // carry by type.
607                            AnyMessageLikeEventContent::RoomMessage(
608                                message.with_relation(item.content.relation()),
609                            )
610                        } else {
611                            return Err(EditError::ContentMismatch {
612                                original: item.content.debug_string().to_owned(),
613                                new: "a message".to_owned(),
614                            }
615                            .into());
616                        }
617                    }
618
619                    EditedContent::PollStart { new_content, .. } => {
620                        if item.content.is_poll() {
621                            AnyMessageLikeEventContent::UnstablePollStart(
622                                UnstablePollStartEventContent::New(
623                                    NewUnstablePollStartEventContent::new(new_content),
624                                ),
625                            )
626                        } else {
627                            return Err(EditError::ContentMismatch {
628                                original: item.content.debug_string().to_owned(),
629                                new: "a poll".to_owned(),
630                            }
631                            .into());
632                        }
633                    }
634
635                    EditedContent::MediaCaption { caption, formatted_caption, mentions } => {
636                        if handle
637                            .edit_media_caption(caption, formatted_caption, mentions)
638                            .await
639                            .map_err(RoomSendQueueError::StorageError)?
640                        {
641                            return Ok(());
642                        }
643                        return Err(EditError::InvalidLocalEchoState.into());
644                    }
645                };
646
647                if !handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)? {
648                    return Err(EditError::InvalidLocalEchoState.into());
649                }
650
651                Ok(())
652            }
653        }
654    }
655
656    /// Toggle a reaction on an event.
657    ///
658    /// Adds or redacts a reaction based on the state of the reaction at the
659    /// time it is called.
660    ///
661    /// When redacting a previous reaction, the redaction reason is not set.
662    ///
663    /// Ensures that only one reaction is sent at a time to avoid race
664    /// conditions and spamming the homeserver with requests.
665    ///
666    /// Returns `true` if the reaction was added, `false` if it was removed.
667    pub async fn toggle_reaction(
668        &self,
669        item_id: &TimelineEventItemId,
670        reaction_key: &str,
671    ) -> Result<bool, Error> {
672        self.controller.toggle_reaction_local(item_id, reaction_key, None).await
673    }
674
675    /// Same as [`Timeline::toggle_reaction`], merging `extra_content`'s fields
676    /// into the reaction's content when one is added.
677    ///
678    /// The reaction's own fields take precedence on conflicts. Removing a
679    /// reaction is a redaction, which carries no content, so `extra_content` is
680    /// only used when adding one — and only for reactions to remote events,
681    /// since a local echo is the user's own not-yet-sent event.
682    pub async fn toggle_reaction_with_extra_content(
683        &self,
684        item_id: &TimelineEventItemId,
685        reaction_key: &str,
686        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
687    ) -> Result<bool, Error> {
688        self.controller.toggle_reaction_local(item_id, reaction_key, extra_content).await
689    }
690
691    /// Sends an attachment to the room.
692    ///
693    /// It does not currently support local echoes.
694    ///
695    /// If the encryption feature is enabled, this method will transparently
696    /// encrypt the room message if the room is encrypted.
697    ///
698    /// The attachment and its optional thumbnail are stored in the media cache
699    /// and can be retrieved at any time, by calling
700    /// [`Media::get_media_content()`] with the `MediaSource` that can be found
701    /// in the corresponding `TimelineEventItem`, and using a
702    /// `MediaFormat::File`.
703    ///
704    /// # Arguments
705    ///
706    /// - `source` - The source of the attachment to send.
707    /// - `mime_type` - The attachment's mime type.
708    /// - `config` - An attachment configuration object containing details about
709    ///   the attachment like a thumbnail, its size, duration etc.
710    ///
711    /// [`Media::get_media_content()`]: matrix_sdk::Media::get_media_content
712    #[instrument(skip_all)]
713    pub fn send_attachment(
714        &self,
715        source: impl Into<AttachmentSource>,
716        mime_type: Mime,
717        config: AttachmentConfig,
718    ) -> SendAttachment<'_> {
719        SendAttachment::new(self, source.into(), mime_type, config)
720    }
721
722    /// Replaces the attachment of a message the current user sent, or adds one
723    /// to a message which had none, through the send queue.
724    ///
725    /// See [`RoomSendQueue::edit_with_attachment()`] for the details. The
726    /// `in_reply_to` of the `config` is ignored: an edit carries no other
727    /// relation.
728    ///
729    /// [`RoomSendQueue::edit_with_attachment()`]: matrix_sdk::send_queue::RoomSendQueue::edit_with_attachment
730    #[instrument(skip_all, fields(%event_id))]
731    pub async fn edit_with_attachment(
732        &self,
733        event_id: &EventId,
734        source: impl Into<AttachmentSource>,
735        mime_type: Mime,
736        config: AttachmentConfig,
737    ) -> Result<(), Error> {
738        let (data, filename) = source.into().try_into_bytes_and_filename()?;
739
740        let config = matrix_sdk::attachment::AttachmentConfig {
741            txn_id: config.txn_id,
742            info: config.info,
743            thumbnail: config.thumbnail,
744            caption: config.caption,
745            mentions: config.mentions,
746            extra_content: config.extra_content,
747            reply: None,
748        };
749
750        self.room()
751            .send_queue()
752            .edit_with_attachment(event_id, filename, mime_type, data, config)
753            .await?;
754
755        Ok(())
756    }
757
758    /// Sends a media gallery to the room.
759    ///
760    /// If the encryption feature is enabled, this method will transparently
761    /// encrypt the room message if the room is encrypted.
762    ///
763    /// The attachments and their optional thumbnails are stored in the media
764    /// cache and can be retrieved at any time, by calling
765    /// [`Media::get_media_content()`] with the `MediaSource` that can be found
766    /// in the corresponding `TimelineEventItem`, and using a
767    /// `MediaFormat::File`.
768    ///
769    /// # Arguments
770    ///
771    /// - `gallery` - A configuration object containing details about the
772    ///   gallery like files, thumbnails, etc.
773    ///
774    /// [`Media::get_media_content()`]: matrix_sdk::Media::get_media_content
775    #[cfg(feature = "unstable-msc4274")]
776    #[instrument(skip_all)]
777    pub fn send_gallery(&self, gallery: GalleryConfig) -> SendGallery<'_> {
778        SendGallery::new(self, gallery)
779    }
780
781    /// Redact an event given its [`TimelineEventItemId`] and an optional
782    /// reason.
783    pub async fn redact(
784        &self,
785        item_id: &TimelineEventItemId,
786        reason: Option<&str>,
787    ) -> Result<(), Error> {
788        let items = self.items().await;
789        let Some((_pos, event)) = rfind_event_by_item_id(&items, item_id) else {
790            return Err(RedactError::ItemNotFound(item_id.clone()).into());
791        };
792
793        match event.handle() {
794            TimelineItemHandle::Remote(event_id) => {
795                self.room()
796                    .send_queue()
797                    .redact(event_id.to_owned(), reason)
798                    .await
799                    .map_err(|_| Error::FailedSendingRedaction)?;
800                Ok(())
801            }
802            TimelineItemHandle::Local(handle) => {
803                // Forward the reason: if the local echo was being sent and the
804                // send wins the race, the server-side redaction that
805                // materializes the abort carries it.
806                if !handle
807                    .abort_with_reason(reason.map(ToOwned::to_owned))
808                    .await
809                    .map_err(RoomSendQueueError::StorageError)?
810                {
811                    return Err(RedactError::InvalidLocalEchoState.into());
812                }
813                Ok(())
814            }
815        }
816    }
817
818    /// Retry sending something on this item that failed, see [`SendTarget`].
819    ///
820    /// Only needed after an unrecoverable failure, which parks the request
821    /// until it's retried or aborted; a recoverable one goes out again when the
822    /// room's send queue is re-enabled.
823    ///
824    /// Returns `false` if there was nothing of that kind left to retry, e.g.
825    /// because it went out in the meantime.
826    pub async fn retry_send(
827        &self,
828        item_id: &TimelineEventItemId,
829        target: SendTarget,
830    ) -> Result<bool, Error> {
831        let Some(handle) = self.controller.pending_send_handle(item_id, target).await? else {
832            return Ok(false);
833        };
834        handle.unwedge().await?;
835        Ok(true)
836    }
837
838    /// Abort sending something on this item that hasn't gone out yet, see
839    /// [`SendTarget`].
840    ///
841    /// Returns `false` if there was nothing of that kind left to abort, e.g.
842    /// because it went out in the meantime.
843    pub async fn abort_send(
844        &self,
845        item_id: &TimelineEventItemId,
846        target: SendTarget,
847    ) -> Result<bool, Error> {
848        let Some(handle) = self.controller.pending_send_handle(item_id, target).await? else {
849            return Ok(false);
850        };
851        handle.abort().await.map_err(|err| Error::SendQueueError(err.into()))
852    }
853
854    /// Fetch unavailable details about the event with the given ID.
855    ///
856    /// This method only works for IDs of remote [`EventTimelineItem`]s, to
857    /// prevent losing details when a local echo is replaced by its remote echo.
858    ///
859    /// This method tries to make all the requests it can. If an error is
860    /// encountered for a given request, it is forwarded with the
861    /// [`TimelineDetails::Error`] variant.
862    ///
863    /// # Arguments
864    ///
865    /// - `event_id` - The event ID of the event to fetch details for.
866    ///
867    /// # Errors
868    ///
869    /// Returns an error if the identifier doesn't match any event with a remote
870    /// echo in the timeline, or if the event is removed from the timeline
871    /// before all requests are handled.
872    #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
873    pub async fn fetch_details_for_event(&self, event_id: &EventId) -> Result<(), Error> {
874        self.controller.fetch_in_reply_to_details(event_id).await
875    }
876
877    /// Fetch all member events for the room this timeline is displaying.
878    ///
879    /// If the full member list is not known, sender profiles are currently
880    /// likely not going to be available. This will be fixed in the future.
881    ///
882    /// If fetching the members fails, any affected timeline items will have the
883    /// `sender_profile` set to [`TimelineDetails::Error`].
884    #[instrument(skip_all)]
885    pub async fn fetch_members(&self) {
886        self.controller.set_sender_profiles_pending().await;
887        match self.room().sync_members().await {
888            Ok(_) => {
889                self.controller.update_missing_sender_profiles().await;
890            }
891            Err(e) => {
892                self.controller.set_sender_profiles_error(Arc::new(e)).await;
893            }
894        }
895    }
896
897    /// Get the latest read receipt for the given user.
898    ///
899    /// Contrary to [`Room::load_user_receipt()`] that only keeps track of read
900    /// receipts received from the homeserver, this keeps also track of implicit
901    /// read receipts in this timeline, i.e. when a room member sends an event.
902    #[instrument(skip(self))]
903    pub async fn latest_user_read_receipt(
904        &self,
905        user_id: &UserId,
906    ) -> Option<(OwnedEventId, Receipt)> {
907        self.controller.latest_user_read_receipt(user_id).await
908    }
909
910    /// Get the ID of the timeline event with the latest read receipt for the
911    /// given user.
912    ///
913    /// In contrary to [`Self::latest_user_read_receipt()`], this allows to know
914    /// the position of the read receipt in the timeline even if the event it
915    /// applies to is not visible in the timeline, unless the event is unknown
916    /// by this timeline.
917    #[instrument(skip(self))]
918    pub async fn latest_user_read_receipt_timeline_event_id(
919        &self,
920        user_id: &UserId,
921    ) -> Option<OwnedEventId> {
922        self.controller.latest_user_read_receipt_timeline_event_id(user_id).await
923    }
924
925    /// Subscribe to changes in the read receipts of our own user.
926    pub async fn subscribe_own_user_read_receipts_changed(&self) -> impl Stream<Item = ()> + use<> {
927        self.controller.subscribe_own_user_read_receipts_changed().await
928    }
929
930    /// Send the given receipt.
931    ///
932    /// This uses [`Room::send_single_receipt`] internally, but checks first if
933    /// the receipt points to an event in this timeline that is more recent than
934    /// the current ones, to avoid unnecessary requests.
935    ///
936    /// If an unthreaded receipt is sent, this will also unset the unread flag
937    /// of the room if necessary.
938    ///
939    /// The thread of the receipt is determined by the timeline instance's focus
940    /// mode and `hide_threaded_events` flag.
941    ///
942    /// Returns a boolean indicating if it sent the receipt or not.
943    #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
944    pub async fn send_single_receipt(
945        &self,
946        receipt_type: ReceiptType,
947        event_id: OwnedEventId,
948    ) -> Result<bool> {
949        self.send_single_receipt_inner(receipt_type, event_id, false).await
950    }
951
952    /// Same as [`Self::send_single_receipt`], but lets the caller state whether
953    /// this is part of marking the whole room as read.
954    ///
955    /// When it is, and the only candidate event is one of the user's own, a
956    /// receipt is still sent against it so the homeserver recomputes its
957    /// push/badge count. See [`TimelineController::should_send_receipt`].
958    async fn send_single_receipt_inner(
959        &self,
960        receipt_type: ReceiptType,
961        event_id: OwnedEventId,
962        is_marking_room_as_read: bool,
963    ) -> Result<bool> {
964        let thread = self.controller.infer_thread_for_read_receipt(&receipt_type);
965
966        let event_id = match self
967            .controller
968            .should_send_receipt(&receipt_type, &thread, &event_id, is_marking_room_as_read)
969            .await
970        {
971            SendReceiptDecision::SendTo(event_id) => event_id,
972            SendReceiptDecision::DoNotSend => {
973                trace!("not sending receipt, because it wouldn't move the real receipt forwards");
974
975                if thread == ReceiptThread::Unthreaded {
976                    // Unset the read marker.
977                    self.room().set_unread_flag(false).await?;
978                }
979
980                return Ok(false);
981            }
982        };
983
984        trace!("sending receipt");
985        self.room().send_single_receipt(receipt_type, thread, event_id).await?;
986        Ok(true)
987    }
988
989    /// Send the given receipts.
990    ///
991    /// This uses [`Room::send_multiple_receipts`] internally, but checks first
992    /// if the receipts point to events in this timeline that are more recent
993    /// than the current ones, to avoid unnecessary requests.
994    ///
995    /// This also unsets the unread marker of the room if necessary.
996    #[instrument(skip(self))]
997    pub async fn send_multiple_receipts(&self, mut receipts: Receipts) -> Result<()> {
998        if let Some(fully_read) = &receipts.fully_read {
999            receipts.fully_read = match self
1000                .controller
1001                .should_send_receipt(
1002                    &ReceiptType::FullyRead,
1003                    &ReceiptThread::Unthreaded,
1004                    fully_read,
1005                    false,
1006                )
1007                .await
1008            {
1009                SendReceiptDecision::SendTo(event_id) => Some(event_id),
1010                SendReceiptDecision::DoNotSend => None,
1011            };
1012        }
1013
1014        if let Some(read_receipt) = &receipts.public_read_receipt {
1015            receipts.public_read_receipt = match self
1016                .controller
1017                .should_send_receipt(
1018                    &ReceiptType::Read,
1019                    &ReceiptThread::Unthreaded,
1020                    read_receipt,
1021                    false,
1022                )
1023                .await
1024            {
1025                SendReceiptDecision::SendTo(event_id) => Some(event_id),
1026                SendReceiptDecision::DoNotSend => None,
1027            };
1028        }
1029
1030        if let Some(private_read_receipt) = &receipts.private_read_receipt {
1031            receipts.private_read_receipt = match self
1032                .controller
1033                .should_send_receipt(
1034                    &ReceiptType::ReadPrivate,
1035                    &ReceiptThread::Unthreaded,
1036                    private_read_receipt,
1037                    false,
1038                )
1039                .await
1040            {
1041                SendReceiptDecision::SendTo(event_id) => Some(event_id),
1042                SendReceiptDecision::DoNotSend => None,
1043            };
1044        }
1045
1046        let room = self.room();
1047
1048        if !receipts.is_empty() {
1049            room.send_multiple_receipts(receipts).await?;
1050        } else {
1051            room.set_unread_flag(false).await?;
1052        }
1053
1054        Ok(())
1055    }
1056
1057    /// Mark the timeline as read by attempting to send a read receipt on the
1058    /// latest visible event.
1059    ///
1060    /// The latest visible event is determined from the timeline's focus kind
1061    /// and whether or not it hides threaded events. If no latest event can be
1062    /// determined and the timeline is live, the room's unread marker is unset
1063    /// instead.
1064    ///
1065    /// # Arguments
1066    ///
1067    /// - `receipt_type` - The type of receipt to send. When using
1068    ///   [`ReceiptType::FullyRead`], an unthreaded receipt will be sent. This
1069    ///   works even if the latest event belongs to a thread, as a threaded
1070    ///   reply also belongs to the unthreaded timeline. Otherwise the
1071    ///   [`ReceiptThread`] will be determined based on the timeline's focus
1072    ///   kind.
1073    ///
1074    /// # Returns
1075    ///
1076    /// A boolean indicating if the receipt was sent or not.
1077    #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
1078    pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<bool> {
1079        if let Some(event_id) = self.controller.latest_event_id().await {
1080            self.send_single_receipt_inner(receipt_type, event_id, true).await
1081        } else {
1082            trace!("can't mark room as read because there's no latest event id");
1083
1084            // For live timelines, unset the read marker in this case.
1085            if self.controller.is_live() {
1086                self.room().set_unread_flag(false).await?;
1087            }
1088
1089            Ok(false)
1090        }
1091    }
1092
1093    /// Create a [`EmbeddedEvent`] from an arbitrary event, be it in the
1094    /// timeline or not.
1095    ///
1096    /// Can be `None` if the event cannot be represented as a standalone item,
1097    /// because it's an aggregation.
1098    pub async fn make_replied_to(
1099        &self,
1100        event: TimelineEvent,
1101    ) -> Result<Option<EmbeddedEvent>, Error> {
1102        self.controller.make_replied_to(event).await
1103    }
1104
1105    /// Returns whether this timeline is focused on a thread (be it live, or
1106    /// from a permalink to a threaded event).
1107    pub fn is_threaded(&self) -> bool {
1108        self.controller.is_threaded()
1109    }
1110}
1111
1112/// Test helpers, likely not very useful in production.
1113#[doc(hidden)]
1114impl Timeline {
1115    /// Get the current list of timeline items.
1116    pub async fn items(&self) -> Vector<Arc<TimelineItem>> {
1117        self.controller.items().await
1118    }
1119
1120    pub async fn subscribe_filter_map<U: Clone>(
1121        &self,
1122        f: impl Fn(Arc<TimelineItem>) -> Option<U>,
1123    ) -> (Vector<U>, impl Stream<Item = VectorDiff<U>>) {
1124        let (items, stream) = self.controller.subscribe_filter_map(f).await;
1125        let stream = TimelineWithDropHandle::new(stream, self.drop_handle.clone());
1126        (items, stream)
1127    }
1128}
1129
1130#[derive(Debug)]
1131struct TimelineDropHandle {
1132    _room_update_join_handle: BackgroundTaskHandle,
1133    #[cfg(feature = "unstable-msc4426")]
1134    _global_profile_updates_handle: BackgroundTaskHandle,
1135    _local_echo_listener_handle: BackgroundTaskHandle,
1136    _rtc_membership_listener_handle: BackgroundTaskHandle,
1137    _event_cache_drop_handle: Arc<EventCacheDropHandles>,
1138    _focus_drop_handle: Option<BackgroundTaskHandle>,
1139    _crypto_drop_handles: CryptoDropHandles,
1140}
1141
1142#[cfg(not(target_family = "wasm"))]
1143pub type TimelineEventFilterFn =
1144    dyn Fn(&AnySyncTimelineEvent, &RoomVersionRules) -> bool + Send + Sync;
1145#[cfg(target_family = "wasm")]
1146pub type TimelineEventFilterFn = dyn Fn(&AnySyncTimelineEvent, &RoomVersionRules) -> bool;
1147
1148/// A source for sending an attachment.
1149///
1150/// The [`AttachmentSource::File`] variant can be constructed from any type that
1151/// implements `Into<PathBuf>`.
1152#[derive(Debug, Clone)]
1153pub enum AttachmentSource {
1154    /// The data of the attachment.
1155    Data {
1156        /// The bytes of the attachment.
1157        bytes: Vec<u8>,
1158
1159        /// The filename of the attachment.
1160        filename: String,
1161    },
1162
1163    /// An attachment loaded from a file.
1164    ///
1165    /// The bytes and the filename will be read from the file at the given path.
1166    File(PathBuf),
1167}
1168
1169impl AttachmentSource {
1170    /// Try to convert this attachment source into a `(bytes, filename)` tuple.
1171    pub(crate) fn try_into_bytes_and_filename(self) -> Result<(Vec<u8>, String), Error> {
1172        match self {
1173            Self::Data { bytes, filename } => Ok((bytes, filename)),
1174            Self::File(path) => {
1175                let filename = path
1176                    .file_name()
1177                    .ok_or(Error::InvalidAttachmentFileName)?
1178                    .to_str()
1179                    .ok_or(Error::InvalidAttachmentFileName)?
1180                    .to_owned();
1181                let bytes = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?;
1182                Ok((bytes, filename))
1183            }
1184        }
1185    }
1186}
1187
1188impl<P> From<P> for AttachmentSource
1189where
1190    P: Into<PathBuf>,
1191{
1192    fn from(value: P) -> Self {
1193        Self::File(value.into())
1194    }
1195}
1196
1197/// Configuration for sending a gallery.
1198///
1199/// This duplicates [`matrix_sdk::attachment::GalleryConfig`] but uses an
1200/// `AttachmentSource` so that we can delay loading the actual data until we're
1201/// inside the SendGallery future. This allows [`Timeline::send_gallery`] to
1202/// return early without blocking the caller.
1203#[cfg(feature = "unstable-msc4274")]
1204#[derive(Debug, Default)]
1205pub struct GalleryConfig {
1206    pub(crate) txn_id: Option<OwnedTransactionId>,
1207    pub(crate) items: Vec<GalleryItemInfo>,
1208    pub(crate) caption: Option<TextMessageEventContent>,
1209    pub(crate) mentions: Option<Mentions>,
1210    pub(crate) in_reply_to: Option<OwnedEventId>,
1211    pub(crate) extra_content: Option<serde_json::Map<String, serde_json::Value>>,
1212}
1213
1214#[cfg(feature = "unstable-msc4274")]
1215impl GalleryConfig {
1216    /// Create a new empty `GalleryConfig`.
1217    pub fn new() -> Self {
1218        Self::default()
1219    }
1220
1221    /// Set the transaction ID to send.
1222    ///
1223    /// # Arguments
1224    ///
1225    /// - `txn_id` - A unique ID that can be attached to a `MessageEvent` held
1226    ///   in its unsigned field as `transaction_id`. If not given, one is
1227    ///   created for the message.
1228    #[must_use]
1229    pub fn txn_id(mut self, txn_id: OwnedTransactionId) -> Self {
1230        self.txn_id = Some(txn_id);
1231        self
1232    }
1233
1234    /// Adds a media item to the gallery.
1235    ///
1236    /// # Arguments
1237    ///
1238    /// * `item` - Information about the item to be added.
1239    #[must_use]
1240    pub fn add_item(mut self, item: GalleryItemInfo) -> Self {
1241        self.items.push(item);
1242        self
1243    }
1244
1245    /// Set the optional caption.
1246    ///
1247    /// # Arguments
1248    ///
1249    /// * `caption` - The optional caption.
1250    pub fn caption(mut self, caption: Option<TextMessageEventContent>) -> Self {
1251        self.caption = caption;
1252        self
1253    }
1254
1255    /// Set the mentions of the message.
1256    ///
1257    /// # Arguments
1258    ///
1259    /// * `mentions` - The mentions of the message.
1260    pub fn mentions(mut self, mentions: Option<Mentions>) -> Self {
1261        self.mentions = mentions;
1262        self
1263    }
1264
1265    /// Set the reply information of the message.
1266    ///
1267    /// # Arguments
1268    ///
1269    /// * `event_id` - The event ID to reply to.
1270    pub fn in_reply_to(mut self, event_id: Option<OwnedEventId>) -> Self {
1271        self.in_reply_to = event_id;
1272        self
1273    }
1274
1275    /// Set additional top-level fields for the gallery event's content.
1276    ///
1277    /// Objects are merged recursively; the event's own fields take precedence
1278    /// on conflicts. To add fields to individual items, use
1279    /// [`GalleryItemInfo::extra_content`].
1280    #[must_use]
1281    pub fn extra_content(
1282        mut self,
1283        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
1284    ) -> Self {
1285        self.extra_content = extra_content;
1286        self
1287    }
1288
1289    /// Returns the number of media items in the gallery.
1290    pub fn len(&self) -> usize {
1291        self.items.len()
1292    }
1293
1294    /// Checks whether the gallery contains any media items or not.
1295    pub fn is_empty(&self) -> bool {
1296        self.items.is_empty()
1297    }
1298}
1299
1300#[cfg(feature = "unstable-msc4274")]
1301#[derive(Debug)]
1302/// Metadata for a gallery item
1303pub struct GalleryItemInfo {
1304    /// The attachment source.
1305    pub source: AttachmentSource,
1306    /// The mime type.
1307    pub content_type: Mime,
1308    /// The attachment info.
1309    pub attachment_info: AttachmentInfo,
1310    /// The caption.
1311    pub caption: Option<TextMessageEventContent>,
1312    /// The thumbnail.
1313    pub thumbnail: Option<Thumbnail>,
1314    /// Additional fields to merge into this item's content, for example a
1315    /// spoiler flag. Objects are merged recursively; the item's own fields take
1316    /// precedence on conflicts.
1317    pub extra_content: Option<serde_json::Map<String, serde_json::Value>>,
1318}
1319
1320#[cfg(feature = "unstable-msc4274")]
1321impl TryFrom<GalleryItemInfo> for matrix_sdk::attachment::GalleryItemInfo {
1322    type Error = Error;
1323
1324    fn try_from(value: GalleryItemInfo) -> Result<Self, Self::Error> {
1325        let (data, filename) = value.source.try_into_bytes_and_filename()?;
1326        Ok(matrix_sdk::attachment::GalleryItemInfo {
1327            filename,
1328            content_type: value.content_type,
1329            data,
1330            attachment_info: value.attachment_info,
1331            caption: value.caption,
1332            thumbnail: value.thumbnail,
1333            extra_content: value.extra_content,
1334        })
1335    }
1336}
1337
1338#[derive(Clone, Debug)]
1339#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1340/// The level of read receipt tracking for the timeline.
1341pub enum TimelineReadReceiptTracking {
1342    /// Track read receipts for all events.
1343    AllEvents,
1344    /// Track read receipts only for message-like events.
1345    MessageLikeEvents,
1346    /// Disable read receipt tracking.
1347    Disabled,
1348}
1349
1350impl TimelineReadReceiptTracking {
1351    /// Whether or not read receipt tracking is enabled.
1352    pub fn is_enabled(&self) -> bool {
1353        match self {
1354            TimelineReadReceiptTracking::AllEvents
1355            | TimelineReadReceiptTracking::MessageLikeEvents => true,
1356            TimelineReadReceiptTracking::Disabled => false,
1357        }
1358    }
1359}