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