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