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}