matrix_sdk_ui/timeline/
mod.rs

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