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