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, 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 poll::unstable_start::{NewUnstablePollStartEventContent, UnstablePollStartEventContent},
48 receipt::{Receipt, ReceiptThread},
49 relation::Thread,
50 room::message::{
51 AddMentions, Relation, RelationWithoutReplacement, ReplyWithinThread,
52 RoomMessageEventContentWithoutRelation, TextMessageEventContent,
53 },
54 },
55 room_version_rules::RoomVersionRules,
56};
57use subscriber::TimelineWithDropHandle;
58use thiserror::Error;
59use tracing::{instrument, trace, warn};
60
61use self::{
62 algorithms::rfind_event_by_id, controller::TimelineController, futures::SendAttachment,
63};
64use crate::timeline::controller::CryptoDropHandles;
65
66mod algorithms;
67mod builder;
68mod controller;
69mod date_dividers;
70mod error;
71pub mod event_filter;
72mod event_handler;
73mod event_item;
74pub mod futures;
75mod item;
76mod latest_event;
77mod pagination;
78mod subscriber;
79mod tasks;
80#[cfg(test)]
81mod tests;
82pub mod thread_list_service;
83mod traits;
84mod virtual_item;
85
86pub use self::{
87 builder::TimelineBuilder,
88 controller::default_event_filter,
89 error::*,
90 event_filter::{TimelineEventCondition, TimelineEventFilter},
91 event_item::{
92 AnyOtherStateEventContentChange, BeaconInfo, EmbeddedEvent, EncryptedMessage,
93 EventItemOrigin, EventSendState, EventTimelineItem, InReplyToDetails, LiveLocationState,
94 MediaUploadProgress, MemberProfileChange, MembershipChange, Message, MsgLikeContent,
95 MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo,
96 ReactionStatus, ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange,
97 Sticker, ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineEventShieldState,
98 TimelineEventShieldStateCode, TimelineItemContent,
99 },
100 item::{TimelineItem, TimelineItemKind, TimelineUniqueId},
101 latest_event::{LatestEventValue, LatestEventValueLocalState},
102 thread_list_service::{ThreadListPaginationState, ThreadListService},
103 traits::RoomExt,
104 virtual_item::VirtualTimelineItem,
105};
106
107/// A high-level view into a regular¹ room's contents.
108///
109/// ¹ This type is meant to be used in the context of rooms without a
110/// `room_type`, that is rooms that are primarily used to exchange text
111/// messages.
112#[derive(Debug)]
113pub struct Timeline {
114 /// Cloneable, inner fields of the `Timeline`, shared with some background
115 /// tasks.
116 controller: TimelineController,
117
118 /// References to long-running tasks held by the timeline.
119 drop_handle: Arc<TimelineDropHandle>,
120}
121
122/// What should the timeline focus on?
123#[derive(Clone, Debug, PartialEq)]
124pub enum TimelineFocus {
125 /// Focus on live events, i.e. receive events from sync and append them in
126 /// real-time.
127 Live {
128 /// Whether to hide in-thread replies from the live timeline.
129 ///
130 /// This should be set to true when the client can create
131 /// [`Self::Thread`]-focused timelines from the thread roots themselves.
132 hide_threaded_events: bool,
133 },
134
135 /// Focus on a specific event, e.g. after clicking a permalink.
136 Event {
137 target: OwnedEventId,
138 num_context_events: u16,
139 /// How to handle threaded events.
140 thread_mode: TimelineEventFocusThreadMode,
141 },
142
143 /// Focus on a specific thread
144 Thread { root_event_id: OwnedEventId },
145
146 /// Only show pinned events.
147 PinnedEvents,
148}
149
150/// Options for controlling the behaviour of [`TimelineFocus::Event`]
151/// for threaded events.
152#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
153#[derive(Clone, Copy, Debug, PartialEq)]
154pub enum TimelineEventFocusThreadMode {
155 /// Force the timeline into threaded mode.
156 ///
157 /// When the focused event is part of a thread, the timeline will be focused
158 /// on that thread's root. Otherwise, the timeline will treat the target
159 /// event itself as the thread root. Threaded events will never be
160 /// hidden.
161 ForceThread,
162
163 /// Automatically determine if the target event is part of a thread or not.
164 ///
165 /// If the event is part of a thread, the timeline
166 /// will be filtered to on-thread events.
167 Automatic {
168 /// When the target event is not part of a thread, whether to
169 /// hide in-thread replies from the live timeline.
170 ///
171 /// Has no effect when the target event is part of a thread.
172 ///
173 /// This should be set to true when the client can create
174 /// [`TimelineFocus::Thread`]-focused timelines from the thread roots
175 /// themselves and doesn't use the [`Self::ForceThread`] mode.
176 hide_threaded_events: bool,
177 },
178}
179
180impl From<TimelineEventFocusThreadMode> for EventFocusThreadMode {
181 fn from(val: TimelineEventFocusThreadMode) -> Self {
182 match val {
183 TimelineEventFocusThreadMode::ForceThread => EventFocusThreadMode::ForceThread,
184 TimelineEventFocusThreadMode::Automatic { .. } => EventFocusThreadMode::Automatic,
185 }
186 }
187}
188
189impl TimelineFocus {
190 pub(super) fn debug_string(&self) -> String {
191 match self {
192 TimelineFocus::Live { .. } => "live".to_owned(),
193 TimelineFocus::Event { target, .. } => format!("permalink:{target}"),
194 TimelineFocus::Thread { root_event_id, .. } => format!("thread:{root_event_id}"),
195 TimelineFocus::PinnedEvents => "pinned-events".to_owned(),
196 }
197 }
198}
199
200/// Changes how dividers get inserted, either in between each day or in between
201/// each month
202#[derive(Debug, Clone)]
203pub enum DateDividerMode {
204 Daily,
205 Monthly,
206}
207
208/// Configuration for sending an attachment.
209///
210/// Like [`matrix_sdk::attachment::AttachmentConfig`], but instead of the
211/// `reply` field, there's only a `in_reply_to` event id; it's the timeline
212/// deciding to fill the rest of the reply parameters.
213#[derive(Debug, Default)]
214pub struct AttachmentConfig {
215 pub txn_id: Option<OwnedTransactionId>,
216 pub info: Option<AttachmentInfo>,
217 pub thumbnail: Option<Thumbnail>,
218 pub caption: Option<TextMessageEventContent>,
219 pub mentions: Option<Mentions>,
220 pub in_reply_to: Option<OwnedEventId>,
221}
222
223impl Timeline {
224 /// Returns the room for this timeline.
225 pub fn room(&self) -> &Room {
226 self.controller.room()
227 }
228
229 /// Clear all timeline items.
230 pub async fn clear(&self) {
231 self.controller.clear().await;
232 }
233
234 /// Retry decryption of previously un-decryptable events given a list of
235 /// session IDs whose keys have been imported.
236 ///
237 /// # Examples
238 ///
239 /// ```no_run
240 /// # use std::{path::PathBuf, time::Duration};
241 /// # use matrix_sdk::{Client, config::SyncSettings, ruma::room_id};
242 /// # use matrix_sdk_ui::Timeline;
243 /// # async {
244 /// # let mut client: Client = todo!();
245 /// # let room_id = ruma::room_id!("!example:example.org");
246 /// # let timeline: Timeline = todo!();
247 /// let path = PathBuf::from("/home/example/e2e-keys.txt");
248 /// let result =
249 /// client.encryption().import_room_keys(path, "secret-passphrase").await?;
250 ///
251 /// // Given a timeline for a specific room_id
252 /// if let Some(keys_for_users) = result.keys.get(room_id) {
253 /// let session_ids = keys_for_users.values().flatten();
254 /// timeline.retry_decryption(session_ids).await;
255 /// }
256 /// # anyhow::Ok(()) };
257 /// ```
258 pub async fn retry_decryption<S: Into<String>>(
259 &self,
260 session_ids: impl IntoIterator<Item = S>,
261 ) {
262 self.controller
263 .retry_event_decryption(Some(session_ids.into_iter().map(Into::into).collect()))
264 .await;
265 }
266
267 #[tracing::instrument(skip(self))]
268 async fn retry_decryption_for_all_events(&self) {
269 self.controller.retry_event_decryption(None).await;
270 }
271
272 /// Get the current timeline item for the given event ID, if any.
273 ///
274 /// Will return a remote event, *or* a local echo that has been sent but not
275 /// yet replaced by a remote echo.
276 ///
277 /// It's preferable to store the timeline items in the model for your UI, if
278 /// possible, instead of just storing IDs and coming back to the timeline
279 /// object to look up items.
280 pub async fn item_by_event_id(&self, event_id: &EventId) -> Option<EventTimelineItem> {
281 let items = self.controller.items().await;
282 let (_, item) = rfind_event_by_id(&items, event_id)?;
283 Some(item.to_owned())
284 }
285
286 /// Get the latest of the timeline's remote event ids.
287 pub async fn latest_event_id(&self) -> Option<OwnedEventId> {
288 self.controller.latest_event_id().await
289 }
290
291 /// Get the current timeline items, along with a stream of updates of
292 /// timeline items.
293 ///
294 /// The stream produces `Vec<VectorDiff<_>>`, which means multiple updates
295 /// at once. There are no delays, it consumes as many updates as possible
296 /// and batches them.
297 pub async fn subscribe(
298 &self,
299 ) -> (Vector<Arc<TimelineItem>>, impl Stream<Item = Vec<VectorDiff<Arc<TimelineItem>>>> + use<>)
300 {
301 let (items, stream) = self.controller.subscribe().await;
302 let stream = TimelineWithDropHandle::new(stream, self.drop_handle.clone());
303 (items, stream)
304 }
305
306 /// Send a message to the room, and add it to the timeline as a local echo.
307 ///
308 /// For simplicity, this method doesn't currently allow custom message
309 /// types.
310 ///
311 /// If the encryption feature is enabled, this method will transparently
312 /// encrypt the room message if the room is encrypted.
313 ///
314 /// If sending the message fails, the local echo item will change its
315 /// `send_state` to [`EventSendState::SendingFailed`].
316 ///
317 /// This will do the right thing in the presence of threads:
318 /// - if this timeline is not focused on a thread, then it will send the
319 /// event as is.
320 /// - if this is a threaded timeline, and the event to send is a room
321 /// message without a relationship, it will automatically mark it as a
322 /// thread reply with the correct reply fallback, and send it.
323 ///
324 /// # Arguments
325 ///
326 /// * `content` - The content of the message event.
327 #[instrument(skip(self, content), fields(room_id = ?self.room().room_id()))]
328 pub async fn send(&self, mut content: AnyMessageLikeEventContent) -> Result<SendHandle, Error> {
329 // If this is a room event we're sending in a threaded timeline, we add the
330 // thread relation ourselves.
331 if content.relation().is_none()
332 && let Some(reply) = self.infer_reply(None).await
333 {
334 match &mut content {
335 AnyMessageLikeEventContent::RoomMessage(room_msg_content) => {
336 content = self
337 .room()
338 .make_reply_event(
339 // Note: this `.into()` gets rid of the relation, but we've checked
340 // previously that the `relates_to` field wasn't
341 // set.
342 room_msg_content.clone().into(),
343 reply,
344 )
345 .await?
346 .into();
347 }
348
349 AnyMessageLikeEventContent::UnstablePollStart(
350 UnstablePollStartEventContent::New(poll),
351 ) => {
352 if let Some(thread_root) = self.controller.thread_root() {
353 poll.relates_to = Some(RelationWithoutReplacement::Thread(Thread::plain(
354 thread_root,
355 reply.event_id,
356 )));
357 }
358 }
359
360 AnyMessageLikeEventContent::Sticker(sticker) => {
361 if let Some(thread_root) = self.controller.thread_root() {
362 sticker.relates_to =
363 Some(Relation::Thread(Thread::plain(thread_root, reply.event_id)));
364 }
365 }
366
367 _ => {}
368 }
369 }
370
371 Ok(self.room().send_queue().send(content).await?)
372 }
373
374 /// Send a reply to the given event.
375 ///
376 /// Currently it only supports events with an event ID and JSON being
377 /// available (which can be removed by local redactions). This is subject to
378 /// change. Use [`EventTimelineItem::can_be_replied_to`] to decide whether
379 /// to render a reply button.
380 ///
381 /// The sender will be added to the mentions of the reply if
382 /// and only if the event has not been written by the sender.
383 ///
384 /// This will do the right thing in the presence of threads:
385 /// - if this timeline is not focused on a thread, then it will forward the
386 /// thread relationship of the replied-to event, if present.
387 /// - if this is a threaded timeline, it will mark the reply as an in-thread
388 /// reply.
389 ///
390 /// # Arguments
391 ///
392 /// * `content` - The content of the reply.
393 ///
394 /// * `in_reply_to` - The ID of the event to reply to.
395 #[instrument(skip(self, content))]
396 pub async fn send_reply(
397 &self,
398 content: RoomMessageEventContentWithoutRelation,
399 in_reply_to: OwnedEventId,
400 ) -> Result<(), Error> {
401 let reply = self
402 .infer_reply(Some(in_reply_to))
403 .await
404 .expect("the reply will always be set because we provided a replied-to event id");
405 let content = self.room().make_reply_event(content, reply).await?;
406 self.send(content.into()).await?;
407 Ok(())
408 }
409
410 /// Given a message or media to send, and an optional `in_reply_to` event,
411 /// automatically fills the [`Reply`] information based on the current
412 /// timeline focus.
413 pub(crate) async fn infer_reply(&self, in_reply_to: Option<OwnedEventId>) -> Option<Reply> {
414 // If there's a replied-to event id, the reply is pretty straightforward, and we
415 // should only infer the `EnforceThread` based on the current focus.
416 if let Some(in_reply_to) = in_reply_to {
417 let enforce_thread = if self.controller.is_threaded() {
418 EnforceThread::Threaded(ReplyWithinThread::Yes)
419 } else {
420 EnforceThread::MaybeThreaded
421 };
422 return Some(Reply {
423 event_id: in_reply_to,
424 enforce_thread,
425 add_mentions: AddMentions::Yes,
426 });
427 }
428
429 let thread_root = self.controller.thread_root()?;
430
431 // The latest event id is used for the reply-to fallback, for clients which
432 // don't handle threads. It should be correctly set to the latest
433 // event in the thread, which the timeline instance might or might
434 // not know about; in this case, we do a best effort of filling it, and resort
435 // to using the thread root if we don't know about any event.
436 //
437 // Note: we could trigger a back-pagination if the timeline is empty, and wait
438 // for the results, if the timeline is too often empty.
439
440 let latest_event_id = self
441 .controller
442 .items()
443 .await
444 .iter()
445 .rev()
446 .find_map(|item| {
447 if let TimelineItemKind::Event(event) = item.kind() {
448 event.event_id().map(ToOwned::to_owned)
449 } else {
450 None
451 }
452 })
453 .unwrap_or(thread_root);
454
455 Some(Reply {
456 event_id: latest_event_id,
457 enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
458 add_mentions: AddMentions::Yes,
459 })
460 }
461
462 /// Edit an event given its [`TimelineEventItemId`] and some new content.
463 ///
464 /// Only supports events for which [`EventTimelineItem::is_editable()`]
465 /// returns `true`.
466 #[instrument(skip(self, new_content))]
467 pub async fn edit(
468 &self,
469 item_id: &TimelineEventItemId,
470 new_content: EditedContent,
471 ) -> Result<(), Error> {
472 let items = self.items().await;
473 let Some((_pos, item)) = rfind_event_by_item_id(&items, item_id) else {
474 return Err(Error::EventNotInTimeline(item_id.clone()));
475 };
476
477 match item.handle() {
478 TimelineItemHandle::Remote(event_id) => {
479 let content = self
480 .room()
481 .make_edit_event(event_id, new_content)
482 .await
483 .map_err(EditError::RoomError)?;
484 self.send(content).await?;
485 Ok(())
486 }
487
488 TimelineItemHandle::Local(handle) => {
489 // Relations are filled by the editing code itself.
490 let new_content: AnyMessageLikeEventContent = match new_content {
491 EditedContent::RoomMessage(message) => {
492 if item.content.is_message() {
493 AnyMessageLikeEventContent::RoomMessage(message.into())
494 } else {
495 return Err(EditError::ContentMismatch {
496 original: item.content.debug_string().to_owned(),
497 new: "a message".to_owned(),
498 }
499 .into());
500 }
501 }
502
503 EditedContent::PollStart { new_content, .. } => {
504 if item.content.is_poll() {
505 AnyMessageLikeEventContent::UnstablePollStart(
506 UnstablePollStartEventContent::New(
507 NewUnstablePollStartEventContent::new(new_content),
508 ),
509 )
510 } else {
511 return Err(EditError::ContentMismatch {
512 original: item.content.debug_string().to_owned(),
513 new: "a poll".to_owned(),
514 }
515 .into());
516 }
517 }
518
519 EditedContent::MediaCaption { caption, formatted_caption, mentions } => {
520 if handle
521 .edit_media_caption(caption, formatted_caption, mentions)
522 .await
523 .map_err(RoomSendQueueError::StorageError)?
524 {
525 return Ok(());
526 }
527 return Err(EditError::InvalidLocalEchoState.into());
528 }
529 };
530
531 if !handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)? {
532 return Err(EditError::InvalidLocalEchoState.into());
533 }
534
535 Ok(())
536 }
537 }
538 }
539
540 /// Toggle a reaction on an event.
541 ///
542 /// Adds or redacts a reaction based on the state of the reaction at the
543 /// time it is called.
544 ///
545 /// When redacting a previous reaction, the redaction reason is not set.
546 ///
547 /// Ensures that only one reaction is sent at a time to avoid race
548 /// conditions and spamming the homeserver with requests.
549 ///
550 /// Returns `true` if the reaction was added, `false` if it was removed.
551 pub async fn toggle_reaction(
552 &self,
553 item_id: &TimelineEventItemId,
554 reaction_key: &str,
555 ) -> Result<bool, Error> {
556 self.controller.toggle_reaction_local(item_id, reaction_key).await
557 }
558
559 /// Sends an attachment to the room.
560 ///
561 /// It does not currently support local echoes.
562 ///
563 /// If the encryption feature is enabled, this method will transparently
564 /// encrypt the room message if the room is encrypted.
565 ///
566 /// The attachment and its optional thumbnail are stored in the media cache
567 /// and can be retrieved at any time, by calling
568 /// [`Media::get_media_content()`] with the `MediaSource` that can be found
569 /// in the corresponding `TimelineEventItem`, and using a
570 /// `MediaFormat::File`.
571 ///
572 /// # Arguments
573 ///
574 /// * `source` - The source of the attachment to send.
575 ///
576 /// * `mime_type` - The attachment's mime type.
577 ///
578 /// * `config` - An attachment configuration object containing details about
579 /// the attachment like a thumbnail, its size, duration etc.
580 ///
581 /// [`Media::get_media_content()`]: matrix_sdk::Media::get_media_content
582 #[instrument(skip_all)]
583 pub fn send_attachment(
584 &self,
585 source: impl Into<AttachmentSource>,
586 mime_type: Mime,
587 config: AttachmentConfig,
588 ) -> SendAttachment<'_> {
589 SendAttachment::new(self, source.into(), mime_type, config)
590 }
591
592 /// Sends a media gallery to the room.
593 ///
594 /// If the encryption feature is enabled, this method will transparently
595 /// encrypt the room message if the room is encrypted.
596 ///
597 /// The attachments and their optional thumbnails are stored in the media
598 /// cache and can be retrieved at any time, by calling
599 /// [`Media::get_media_content()`] with the `MediaSource` that can be found
600 /// in the corresponding `TimelineEventItem`, and using a
601 /// `MediaFormat::File`.
602 ///
603 /// # Arguments
604 /// * `gallery` - A configuration object containing details about the
605 /// gallery like files, thumbnails, etc.
606 ///
607 /// [`Media::get_media_content()`]: matrix_sdk::Media::get_media_content
608 #[cfg(feature = "unstable-msc4274")]
609 #[instrument(skip_all)]
610 pub fn send_gallery(&self, gallery: GalleryConfig) -> SendGallery<'_> {
611 SendGallery::new(self, gallery)
612 }
613
614 /// Redact an event given its [`TimelineEventItemId`] and an optional
615 /// reason.
616 pub async fn redact(
617 &self,
618 item_id: &TimelineEventItemId,
619 reason: Option<&str>,
620 ) -> Result<(), Error> {
621 let items = self.items().await;
622 let Some((_pos, event)) = rfind_event_by_item_id(&items, item_id) else {
623 return Err(RedactError::ItemNotFound(item_id.clone()).into());
624 };
625
626 match event.handle() {
627 TimelineItemHandle::Remote(event_id) => {
628 self.room().redact(event_id, reason, None).await.map_err(RedactError::HttpError)?;
629 }
630 TimelineItemHandle::Local(handle) => {
631 if !handle.abort().await.map_err(RoomSendQueueError::StorageError)? {
632 return Err(RedactError::InvalidLocalEchoState.into());
633 }
634 }
635 }
636
637 Ok(())
638 }
639
640 /// Fetch unavailable details about the event with the given ID.
641 ///
642 /// This method only works for IDs of remote [`EventTimelineItem`]s,
643 /// to prevent losing details when a local echo is replaced by its
644 /// remote echo.
645 ///
646 /// This method tries to make all the requests it can. If an error is
647 /// encountered for a given request, it is forwarded with the
648 /// [`TimelineDetails::Error`] variant.
649 ///
650 /// # Arguments
651 ///
652 /// * `event_id` - The event ID of the event to fetch details for.
653 ///
654 /// # Errors
655 ///
656 /// Returns an error if the identifier doesn't match any event with a remote
657 /// echo in the timeline, or if the event is removed from the timeline
658 /// before all requests are handled.
659 #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
660 pub async fn fetch_details_for_event(&self, event_id: &EventId) -> Result<(), Error> {
661 self.controller.fetch_in_reply_to_details(event_id).await
662 }
663
664 /// Fetch all member events for the room this timeline is displaying.
665 ///
666 /// If the full member list is not known, sender profiles are currently
667 /// likely not going to be available. This will be fixed in the future.
668 ///
669 /// If fetching the members fails, any affected timeline items will have
670 /// the `sender_profile` set to [`TimelineDetails::Error`].
671 #[instrument(skip_all)]
672 pub async fn fetch_members(&self) {
673 self.controller.set_sender_profiles_pending().await;
674 match self.room().sync_members().await {
675 Ok(_) => {
676 self.controller.update_missing_sender_profiles().await;
677 }
678 Err(e) => {
679 self.controller.set_sender_profiles_error(Arc::new(e)).await;
680 }
681 }
682 }
683
684 /// Get the latest read receipt for the given user.
685 ///
686 /// Contrary to [`Room::load_user_receipt()`] that only keeps track of read
687 /// receipts received from the homeserver, this keeps also track of implicit
688 /// read receipts in this timeline, i.e. when a room member sends an event.
689 #[instrument(skip(self))]
690 pub async fn latest_user_read_receipt(
691 &self,
692 user_id: &UserId,
693 ) -> Option<(OwnedEventId, Receipt)> {
694 self.controller.latest_user_read_receipt(user_id).await
695 }
696
697 /// Get the ID of the timeline event with the latest read receipt for the
698 /// given user.
699 ///
700 /// In contrary to [`Self::latest_user_read_receipt()`], this allows to know
701 /// the position of the read receipt in the timeline even if the event it
702 /// applies to is not visible in the timeline, unless the event is unknown
703 /// by this timeline.
704 #[instrument(skip(self))]
705 pub async fn latest_user_read_receipt_timeline_event_id(
706 &self,
707 user_id: &UserId,
708 ) -> Option<OwnedEventId> {
709 self.controller.latest_user_read_receipt_timeline_event_id(user_id).await
710 }
711
712 /// Subscribe to changes in the read receipts of our own user.
713 pub async fn subscribe_own_user_read_receipts_changed(&self) -> impl Stream<Item = ()> + use<> {
714 self.controller.subscribe_own_user_read_receipts_changed().await
715 }
716
717 /// Send the given receipt.
718 ///
719 /// This uses [`Room::send_single_receipt`] internally, but checks
720 /// first if the receipt points to an event in this timeline that is more
721 /// recent than the current ones, to avoid unnecessary requests.
722 ///
723 /// If an unthreaded receipt is sent, this will also unset the unread flag
724 /// of the room if necessary.
725 ///
726 /// The thread of the receipt is determined by the timeline instance's
727 /// focus mode and `hide_threaded_events` flag.
728 ///
729 /// Returns a boolean indicating if it sent the receipt or not.
730 #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
731 pub async fn send_single_receipt(
732 &self,
733 receipt_type: ReceiptType,
734 event_id: OwnedEventId,
735 ) -> Result<bool> {
736 let thread = self.controller.infer_thread_for_read_receipt(&receipt_type);
737
738 if !self.controller.should_send_receipt(&receipt_type, &thread, &event_id).await {
739 trace!(
740 "not sending receipt, because we already cover the event with a previous receipt"
741 );
742
743 if thread == ReceiptThread::Unthreaded {
744 // Unset the read marker.
745 self.room().set_unread_flag(false).await?;
746 }
747
748 return Ok(false);
749 }
750
751 trace!("sending receipt");
752 self.room().send_single_receipt(receipt_type, thread, event_id).await?;
753 Ok(true)
754 }
755
756 /// Send the given receipts.
757 ///
758 /// This uses [`Room::send_multiple_receipts`] internally, but
759 /// checks first if the receipts point to events in this timeline that
760 /// are more recent than the current ones, to avoid unnecessary
761 /// requests.
762 ///
763 /// This also unsets the unread marker of the room if necessary.
764 #[instrument(skip(self))]
765 pub async fn send_multiple_receipts(&self, mut receipts: Receipts) -> Result<()> {
766 if let Some(fully_read) = &receipts.fully_read
767 && !self
768 .controller
769 .should_send_receipt(
770 &ReceiptType::FullyRead,
771 &ReceiptThread::Unthreaded,
772 fully_read,
773 )
774 .await
775 {
776 receipts.fully_read = None;
777 }
778
779 if let Some(read_receipt) = &receipts.public_read_receipt
780 && !self
781 .controller
782 .should_send_receipt(&ReceiptType::Read, &ReceiptThread::Unthreaded, read_receipt)
783 .await
784 {
785 receipts.public_read_receipt = None;
786 }
787
788 if let Some(private_read_receipt) = &receipts.private_read_receipt
789 && !self
790 .controller
791 .should_send_receipt(
792 &ReceiptType::ReadPrivate,
793 &ReceiptThread::Unthreaded,
794 private_read_receipt,
795 )
796 .await
797 {
798 receipts.private_read_receipt = None;
799 }
800
801 let room = self.room();
802
803 if !receipts.is_empty() {
804 room.send_multiple_receipts(receipts).await?;
805 } else {
806 room.set_unread_flag(false).await?;
807 }
808
809 Ok(())
810 }
811
812 /// Mark the timeline as read by attempting to send a read receipt on the
813 /// latest visible event.
814 ///
815 /// The latest visible event is determined from the timeline's focus kind
816 /// and whether or not it hides threaded events. If no latest event can
817 /// be determined and the timeline is live, the room's unread marker is
818 /// unset instead.
819 ///
820 /// # Arguments
821 ///
822 /// * `receipt_type` - The type of receipt to send. When using
823 /// [`ReceiptType::FullyRead`], an unthreaded receipt will be sent. This
824 /// works even if the latest event belongs to a thread, as a threaded
825 /// reply also belongs to the unthreaded timeline. Otherwise the
826 /// [`ReceiptThread`] will be determined based on the timeline's focus
827 /// kind.
828 ///
829 /// # Returns
830 ///
831 /// A boolean indicating if the receipt was sent or not.
832 #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
833 pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<bool> {
834 if let Some(event_id) = self.controller.latest_event_id().await {
835 self.send_single_receipt(receipt_type, event_id).await
836 } else {
837 trace!("can't mark room as read because there's no latest event id");
838
839 // For live timelines, unset the read marker in this case.
840 if self.controller.is_live() {
841 self.room().set_unread_flag(false).await?;
842 }
843
844 Ok(false)
845 }
846 }
847
848 /// Create a [`EmbeddedEvent`] from an arbitrary event, be it in the
849 /// timeline or not.
850 ///
851 /// Can be `None` if the event cannot be represented as a standalone item,
852 /// because it's an aggregation.
853 pub async fn make_replied_to(
854 &self,
855 event: TimelineEvent,
856 ) -> Result<Option<EmbeddedEvent>, Error> {
857 self.controller.make_replied_to(event).await
858 }
859
860 /// Returns whether this timeline is focused on a thread (be it live, or
861 /// from a permalink to a threaded event).
862 pub fn is_threaded(&self) -> bool {
863 self.controller.is_threaded()
864 }
865}
866
867/// Test helpers, likely not very useful in production.
868#[doc(hidden)]
869impl Timeline {
870 /// Get the current list of timeline items.
871 pub async fn items(&self) -> Vector<Arc<TimelineItem>> {
872 self.controller.items().await
873 }
874
875 pub async fn subscribe_filter_map<U: Clone>(
876 &self,
877 f: impl Fn(Arc<TimelineItem>) -> Option<U>,
878 ) -> (Vector<U>, impl Stream<Item = VectorDiff<U>>) {
879 let (items, stream) = self.controller.subscribe_filter_map(f).await;
880 let stream = TimelineWithDropHandle::new(stream, self.drop_handle.clone());
881 (items, stream)
882 }
883}
884
885#[derive(Debug)]
886struct TimelineDropHandle {
887 _room_update_join_handle: BackgroundTaskHandle,
888 _local_echo_listener_handle: BackgroundTaskHandle,
889 _event_cache_drop_handle: Arc<EventCacheDropHandles>,
890 _focus_drop_handle: Option<BackgroundTaskHandle>,
891 _crypto_drop_handles: CryptoDropHandles,
892}
893
894#[cfg(not(target_family = "wasm"))]
895pub type TimelineEventFilterFn =
896 dyn Fn(&AnySyncTimelineEvent, &RoomVersionRules) -> bool + Send + Sync;
897#[cfg(target_family = "wasm")]
898pub type TimelineEventFilterFn = dyn Fn(&AnySyncTimelineEvent, &RoomVersionRules) -> bool;
899
900/// A source for sending an attachment.
901///
902/// The [`AttachmentSource::File`] variant can be constructed from any type that
903/// implements `Into<PathBuf>`.
904#[derive(Debug, Clone)]
905pub enum AttachmentSource {
906 /// The data of the attachment.
907 Data {
908 /// The bytes of the attachment.
909 bytes: Vec<u8>,
910
911 /// The filename of the attachment.
912 filename: String,
913 },
914
915 /// An attachment loaded from a file.
916 ///
917 /// The bytes and the filename will be read from the file at the given path.
918 File(PathBuf),
919}
920
921impl AttachmentSource {
922 /// Try to convert this attachment source into a `(bytes, filename)` tuple.
923 pub(crate) fn try_into_bytes_and_filename(self) -> Result<(Vec<u8>, String), Error> {
924 match self {
925 Self::Data { bytes, filename } => Ok((bytes, filename)),
926 Self::File(path) => {
927 let filename = path
928 .file_name()
929 .ok_or(Error::InvalidAttachmentFileName)?
930 .to_str()
931 .ok_or(Error::InvalidAttachmentFileName)?
932 .to_owned();
933 let bytes = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?;
934 Ok((bytes, filename))
935 }
936 }
937 }
938}
939
940impl<P> From<P> for AttachmentSource
941where
942 P: Into<PathBuf>,
943{
944 fn from(value: P) -> Self {
945 Self::File(value.into())
946 }
947}
948
949/// Configuration for sending a gallery.
950///
951/// This duplicates [`matrix_sdk::attachment::GalleryConfig`] but uses an
952/// `AttachmentSource` so that we can delay loading the actual data until we're
953/// inside the SendGallery future. This allows [`Timeline::send_gallery`] to
954/// return early without blocking the caller.
955#[cfg(feature = "unstable-msc4274")]
956#[derive(Debug, Default)]
957pub struct GalleryConfig {
958 pub(crate) txn_id: Option<OwnedTransactionId>,
959 pub(crate) items: Vec<GalleryItemInfo>,
960 pub(crate) caption: Option<TextMessageEventContent>,
961 pub(crate) mentions: Option<Mentions>,
962 pub(crate) in_reply_to: Option<OwnedEventId>,
963}
964
965#[cfg(feature = "unstable-msc4274")]
966impl GalleryConfig {
967 /// Create a new empty `GalleryConfig`.
968 pub fn new() -> Self {
969 Self::default()
970 }
971
972 /// Set the transaction ID to send.
973 ///
974 /// # Arguments
975 ///
976 /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` held
977 /// in its unsigned field as `transaction_id`. If not given, one is
978 /// created for the message.
979 #[must_use]
980 pub fn txn_id(mut self, txn_id: OwnedTransactionId) -> Self {
981 self.txn_id = Some(txn_id);
982 self
983 }
984
985 /// Adds a media item to the gallery.
986 ///
987 /// # Arguments
988 ///
989 /// * `item` - Information about the item to be added.
990 #[must_use]
991 pub fn add_item(mut self, item: GalleryItemInfo) -> Self {
992 self.items.push(item);
993 self
994 }
995
996 /// Set the optional caption.
997 ///
998 /// # Arguments
999 ///
1000 /// * `caption` - The optional caption.
1001 pub fn caption(mut self, caption: Option<TextMessageEventContent>) -> Self {
1002 self.caption = caption;
1003 self
1004 }
1005
1006 /// Set the mentions of the message.
1007 ///
1008 /// # Arguments
1009 ///
1010 /// * `mentions` - The mentions of the message.
1011 pub fn mentions(mut self, mentions: Option<Mentions>) -> Self {
1012 self.mentions = mentions;
1013 self
1014 }
1015
1016 /// Set the reply information of the message.
1017 ///
1018 /// # Arguments
1019 ///
1020 /// * `event_id` - The event ID to reply to.
1021 pub fn in_reply_to(mut self, event_id: Option<OwnedEventId>) -> Self {
1022 self.in_reply_to = event_id;
1023 self
1024 }
1025
1026 /// Returns the number of media items in the gallery.
1027 pub fn len(&self) -> usize {
1028 self.items.len()
1029 }
1030
1031 /// Checks whether the gallery contains any media items or not.
1032 pub fn is_empty(&self) -> bool {
1033 self.items.is_empty()
1034 }
1035}
1036
1037#[cfg(feature = "unstable-msc4274")]
1038#[derive(Debug)]
1039/// Metadata for a gallery item
1040pub struct GalleryItemInfo {
1041 /// The attachment source.
1042 pub source: AttachmentSource,
1043 /// The mime type.
1044 pub content_type: Mime,
1045 /// The attachment info.
1046 pub attachment_info: AttachmentInfo,
1047 /// The caption.
1048 pub caption: Option<TextMessageEventContent>,
1049 /// The thumbnail.
1050 pub thumbnail: Option<Thumbnail>,
1051}
1052
1053#[cfg(feature = "unstable-msc4274")]
1054impl TryFrom<GalleryItemInfo> for matrix_sdk::attachment::GalleryItemInfo {
1055 type Error = Error;
1056
1057 fn try_from(value: GalleryItemInfo) -> Result<Self, Self::Error> {
1058 let (data, filename) = value.source.try_into_bytes_and_filename()?;
1059 Ok(matrix_sdk::attachment::GalleryItemInfo {
1060 filename,
1061 content_type: value.content_type,
1062 data,
1063 attachment_info: value.attachment_info,
1064 caption: value.caption,
1065 thumbnail: value.thumbnail,
1066 })
1067 }
1068}
1069
1070#[derive(Clone, Debug)]
1071#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1072/// The level of read receipt tracking for the timeline.
1073pub enum TimelineReadReceiptTracking {
1074 /// Track read receipts for all events.
1075 AllEvents,
1076 /// Track read receipts only for message-like events.
1077 MessageLikeEvents,
1078 /// Disable read receipt tracking.
1079 Disabled,
1080}
1081
1082impl TimelineReadReceiptTracking {
1083 /// Whether or not read receipt tracking is enabled.
1084 pub fn is_enabled(&self) -> bool {
1085 match self {
1086 TimelineReadReceiptTracking::AllEvents
1087 | TimelineReadReceiptTracking::MessageLikeEvents => true,
1088 TimelineReadReceiptTracking::Disabled => false,
1089 }
1090 }
1091}