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