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