Skip to main content

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