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