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