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