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