matrix_sdk_ui/timeline/
mod.rs

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