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, iter, 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        location::{AssetType, LocationContent, ZoomLevel},
48        poll::unstable_start::{NewUnstablePollStartEventContent, UnstablePollStartEventContent},
49        receipt::{Receipt, ReceiptThread},
50        relation::{RelationType, Thread},
51        room::message::{
52            AddMentions, LocationMessageEventContent, MessageType, Relation,
53            RelationWithoutReplacement, ReplyWithinThread, RoomMessageEventContent,
54            RoomMessageEventContentWithoutRelation, TextMessageEventContent,
55        },
56    },
57    room_version_rules::RoomVersionRules,
58};
59use subscriber::TimelineWithDropHandle;
60use thiserror::Error;
61use tracing::{instrument, trace, warn};
62
63use self::{
64    algorithms::rfind_event_by_id, controller::TimelineController, futures::SendAttachment,
65};
66use crate::timeline::controller::{CryptoDropHandles, SendReceiptDecision};
67
68mod algorithms;
69mod builder;
70mod controller;
71mod date_dividers;
72mod error;
73pub mod event_filter;
74mod event_handler;
75mod event_item;
76pub mod futures;
77mod item;
78mod latest_event;
79mod pagination;
80mod subscriber;
81mod tasks;
82#[cfg(test)]
83mod tests;
84pub mod thread_list_service;
85mod traits;
86mod virtual_item;
87
88pub use self::{
89    builder::TimelineBuilder,
90    controller::default_event_filter,
91    error::*,
92    event_filter::{TimelineEventCondition, TimelineEventFilter},
93    event_item::{
94        AnyOtherStateEventContentChange, BeaconInfo, EditRevision, EmbeddedEvent, EncryptedMessage,
95        EventItemOrigin, EventSendState, EventTimelineItem, InReplyToDetails, LiveLocationState,
96        MediaUploadProgress, MemberProfileChange, MembershipChange, Message, MsgLikeContent,
97        MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo,
98        ReactionStatus, ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange,
99        Sticker, ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineEventShieldState,
100        TimelineEventShieldStateCode, TimelineItemContent,
101    },
102    item::{TimelineItem, TimelineItemKind, TimelineUniqueId},
103    latest_event::{LatestEventValue, LatestEventValueLocalState},
104    thread_list_service::{ThreadListPaginationState, ThreadListService},
105    traits::RoomExt,
106    virtual_item::VirtualTimelineItem,
107};
108
109/// A high-level view into a regular¹ room's contents.
110///
111/// ¹ This type is meant to be used in the context of rooms without a
112/// `room_type`, that is rooms that are primarily used to exchange text
113/// messages.
114#[derive(Debug)]
115pub struct Timeline {
116    /// Cloneable, inner fields of the `Timeline`, shared with some background
117    /// tasks.
118    controller: TimelineController,
119
120    /// References to long-running tasks held by the timeline.
121    drop_handle: Arc<TimelineDropHandle>,
122}
123
124/// What should the timeline focus on?
125#[derive(Clone, Debug, PartialEq)]
126pub enum TimelineFocus {
127    /// Focus on live events, i.e. receive events from sync and append them in
128    /// real-time.
129    Live {
130        /// Whether to hide in-thread replies from the live timeline.
131        ///
132        /// This should be set to true when the client can create
133        /// [`Self::Thread`]-focused timelines from the thread roots themselves.
134        hide_threaded_events: bool,
135    },
136
137    /// Focus on a specific event, e.g. after clicking a permalink.
138    Event {
139        target: OwnedEventId,
140        num_context_events: u16,
141        /// How to handle threaded events.
142        thread_mode: TimelineEventFocusThreadMode,
143    },
144
145    /// Focus on a specific thread
146    Thread { root_event_id: OwnedEventId },
147
148    /// Only show pinned events.
149    PinnedEvents,
150}
151
152/// Options for controlling the behaviour of [`TimelineFocus::Event`]
153/// for threaded events.
154#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
155#[derive(Clone, Copy, Debug, PartialEq)]
156pub enum TimelineEventFocusThreadMode {
157    /// Force the timeline into threaded mode.
158    ///
159    /// When the focused event is part of a thread, the timeline will be focused
160    /// on that thread's root. Otherwise, the timeline will treat the target
161    /// event itself as the thread root. Threaded events will never be
162    /// hidden.
163    ForceThread,
164
165    /// Automatically determine if the target event is part of a thread or not.
166    ///
167    /// 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.
172        ///
173        /// Has no effect when the target event is part of a thread.
174        ///
175        /// This should be set to true when the client can create
176        /// [`TimelineFocus::Thread`]-focused timelines from the thread roots
177        /// themselves and doesn't use the [`Self::ForceThread`] mode.
178        hide_threaded_events: bool,
179    },
180}
181
182impl From<TimelineEventFocusThreadMode> for EventFocusThreadMode {
183    fn from(val: TimelineEventFocusThreadMode) -> Self {
184        match val {
185            TimelineEventFocusThreadMode::ForceThread => EventFocusThreadMode::ForceThread,
186            TimelineEventFocusThreadMode::Automatic { .. } => EventFocusThreadMode::Automatic,
187        }
188    }
189}
190
191impl TimelineFocus {
192    pub(super) fn debug_string(&self) -> String {
193        match self {
194            TimelineFocus::Live { .. } => "live".to_owned(),
195            TimelineFocus::Event { target, .. } => format!("permalink:{target}"),
196            TimelineFocus::Thread { root_event_id, .. } => format!("thread:{root_event_id}"),
197            TimelineFocus::PinnedEvents => "pinned-events".to_owned(),
198        }
199    }
200}
201
202/// Changes how dividers get inserted, either in between each day or in between
203/// each month
204#[derive(Debug, Clone)]
205pub enum DateDividerMode {
206    Daily,
207    Monthly,
208}
209
210/// Configuration for sending an attachment.
211///
212/// Like [`matrix_sdk::attachment::AttachmentConfig`], but instead of the
213/// `reply` field, there's only a `in_reply_to` event id; it's the timeline
214/// deciding to fill the rest of the reply parameters.
215#[derive(Debug, Default)]
216pub struct AttachmentConfig {
217    pub txn_id: Option<OwnedTransactionId>,
218    pub info: Option<AttachmentInfo>,
219    pub thumbnail: Option<Thumbnail>,
220    pub caption: Option<TextMessageEventContent>,
221    pub mentions: Option<Mentions>,
222    pub in_reply_to: Option<OwnedEventId>,
223    pub extra_content: Option<serde_json::Map<String, serde_json::Value>>,
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 edit history for the given event.
290    ///
291    /// Returns all revisions of the event, in chronological order.
292    /// The first entry is the original event content, followed by each
293    /// edit in the order they were applied.
294    ///
295    /// This looks up the event and all `m.replace` relations targeting it,
296    /// first in the event cache and falling back to the homeserver if needed.
297    /// This works regardless of the timeline's focus kind (live, thread,
298    /// permalink, or pinned events).
299    pub async fn edit_revisions(&self, event_id: &EventId) -> Result<Vec<EditRevision>, Error> {
300        let Ok((original_event, edit_events)) = self
301            .controller
302            .find_event_with_relations(event_id, Some(vec![RelationType::Replacement]))
303            .await
304        else {
305            return Ok(Vec::new());
306        };
307
308        let room = self.room();
309        let mut revisions = Vec::with_capacity(edit_events.len() + 1);
310
311        for event in iter::once(original_event).chain(edit_events) {
312            let timestamp = event.timestamp();
313            if let Some(content) = TimelineItemContent::from_event(room, event).await {
314                revisions.push(EditRevision { content, timestamp });
315            }
316        }
317
318        Ok(revisions)
319    }
320
321    /// Get the latest of the timeline's remote event ids.
322    pub async fn latest_event_id(&self) -> Option<OwnedEventId> {
323        self.controller.latest_event_id().await
324    }
325
326    /// Get the current timeline items, along with a stream of updates of
327    /// timeline items.
328    ///
329    /// The stream produces `Vec<VectorDiff<_>>`, which means multiple updates
330    /// at once. There are no delays, it consumes as many updates as possible
331    /// and batches them.
332    pub async fn subscribe(
333        &self,
334    ) -> (Vector<Arc<TimelineItem>>, impl Stream<Item = Vec<VectorDiff<Arc<TimelineItem>>>> + use<>)
335    {
336        let (items, stream) = self.controller.subscribe().await;
337        let stream = TimelineWithDropHandle::new(stream, self.drop_handle.clone());
338        (items, stream)
339    }
340
341    /// Send a message to the room, and add it to the timeline as a local echo.
342    ///
343    /// For simplicity, this method doesn't currently allow custom message
344    /// types.
345    ///
346    /// If the encryption feature is enabled, this method will transparently
347    /// encrypt the room message if the room is encrypted.
348    ///
349    /// If sending the message fails, the local echo item will change its
350    /// `send_state` to [`EventSendState::SendingFailed`].
351    ///
352    /// This will do the right thing in the presence of threads:
353    /// - if this timeline is not focused on a thread, then it will send the
354    ///   event as is.
355    /// - if this is a threaded timeline, and the event to send is a room
356    ///   message without a relationship, it will automatically mark it as a
357    ///   thread reply with the correct reply fallback, and send it.
358    ///
359    /// # Arguments
360    ///
361    /// * `content` - The content of the message event.
362    #[instrument(skip(self, content), fields(room_id = ?self.room().room_id()))]
363    pub async fn send(&self, content: AnyMessageLikeEventContent) -> Result<SendHandle, Error> {
364        self.send_with_extra_content(content, None).await
365    }
366
367    /// Queues an event in this room's send queue, with additional top-level
368    /// fields merged into its content. The event's own fields take precedence
369    /// on conflicts.
370    ///
371    /// See [`Self::send`] for more details.
372    #[instrument(skip(self, content, extra_content), fields(room_id = ?self.room().room_id()))]
373    pub async fn send_with_extra_content(
374        &self,
375        mut content: AnyMessageLikeEventContent,
376        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
377    ) -> Result<SendHandle, Error> {
378        // If this is a room event we're sending in a threaded timeline, we add the
379        // thread relation ourselves.
380        if content.relation().is_none()
381            && let Some(reply) = self.infer_reply(None).await
382        {
383            match &mut content {
384                AnyMessageLikeEventContent::RoomMessage(room_msg_content) => {
385                    content = self
386                        .room()
387                        .make_reply_event(
388                            // Note: this `.into()` gets rid of the relation, but we've checked
389                            // previously that the `relates_to` field wasn't
390                            // set.
391                            room_msg_content.clone().into(),
392                            reply,
393                        )
394                        .await?
395                        .into();
396                }
397
398                AnyMessageLikeEventContent::UnstablePollStart(
399                    UnstablePollStartEventContent::New(poll),
400                ) => {
401                    if let Some(thread_root) = self.controller.thread_root() {
402                        poll.relates_to = Some(RelationWithoutReplacement::Thread(Thread::plain(
403                            thread_root,
404                            reply.event_id,
405                        )));
406                    }
407                }
408
409                AnyMessageLikeEventContent::Sticker(sticker) => {
410                    if let Some(thread_root) = self.controller.thread_root() {
411                        sticker.relates_to =
412                            Some(Relation::Thread(Thread::plain(thread_root, reply.event_id)));
413                    }
414                }
415
416                _ => {}
417            }
418        }
419
420        let queue = self.room().send_queue();
421        let send = queue.send(content);
422        let send = match extra_content {
423            Some(extra_content) => send.with_extra_content(extra_content),
424            None => send,
425        };
426        Ok(send.await?)
427    }
428
429    /// Send a reply to the given event.
430    ///
431    /// Currently it only supports events with an event ID and JSON being
432    /// available (which can be removed by local redactions). This is subject to
433    /// change. Use [`EventTimelineItem::can_be_replied_to`] to decide whether
434    /// to render a reply button.
435    ///
436    /// The sender will be added to the mentions of the reply if
437    /// and only if the event has not been written by the sender.
438    ///
439    /// This will do the right thing in the presence of threads:
440    /// - if this timeline is not focused on a thread, then it will forward the
441    ///   thread relationship of the replied-to event, if present.
442    /// - if this is a threaded timeline, it will mark the reply as an in-thread
443    ///   reply.
444    ///
445    /// # Arguments
446    ///
447    /// * `content` - The content of the reply.
448    ///
449    /// * `in_reply_to` - The ID of the event to reply to.
450    #[instrument(skip(self, content))]
451    pub async fn send_reply(
452        &self,
453        content: RoomMessageEventContentWithoutRelation,
454        in_reply_to: OwnedEventId,
455    ) -> Result<SendHandle, Error> {
456        let reply = self
457            .infer_reply(Some(in_reply_to))
458            .await
459            .expect("the reply will always be set because we provided a replied-to event id");
460        let content = self.room().make_reply_event(content, reply).await?;
461        self.send(content.into()).await
462    }
463
464    /// Send a location event to the room, with `body` as the plain-text
465    /// fallback and `geo_uri` its RFC 5870 representation. With `in_reply_to`,
466    /// the location is sent as a reply, with [`Self::send_reply`] semantics.
467    #[instrument(skip(self, body, geo_uri, description))]
468    pub async fn send_location(
469        &self,
470        body: String,
471        geo_uri: String,
472        description: Option<String>,
473        zoom_level: Option<ZoomLevel>,
474        asset_type: Option<AssetType>,
475        in_reply_to: Option<OwnedEventId>,
476    ) -> Result<SendHandle, Error> {
477        let mut content = LocationMessageEventContent::new(body, geo_uri.clone());
478
479        if let Some(asset_type) = asset_type {
480            content = content.with_asset_type(asset_type);
481        }
482
483        let mut location = LocationContent::new(geo_uri);
484        location.description = description;
485        location.zoom_level = zoom_level;
486        content.location = Some(location);
487
488        let msgtype = MessageType::Location(content);
489
490        match in_reply_to {
491            Some(event_id) => {
492                self.send_reply(RoomMessageEventContentWithoutRelation::new(msgtype), event_id)
493                    .await
494            }
495            None => self.send(RoomMessageEventContent::new(msgtype).into()).await,
496        }
497    }
498
499    /// Given a message or media to send, and an optional `in_reply_to` event,
500    /// automatically fills the [`Reply`] information based on the current
501    /// timeline focus.
502    pub(crate) async fn infer_reply(&self, in_reply_to: Option<OwnedEventId>) -> Option<Reply> {
503        // If there's a replied-to event id, the reply is pretty straightforward, and we
504        // should only infer the `EnforceThread` based on the current focus.
505        if let Some(in_reply_to) = in_reply_to {
506            let enforce_thread = if self.controller.is_threaded() {
507                EnforceThread::Threaded(ReplyWithinThread::Yes)
508            } else {
509                EnforceThread::MaybeThreaded
510            };
511            return Some(Reply {
512                event_id: in_reply_to,
513                enforce_thread,
514                add_mentions: AddMentions::Yes,
515            });
516        }
517
518        let thread_root = self.controller.thread_root()?;
519
520        // The latest event id is used for the reply-to fallback, for clients which
521        // don't handle threads. It should be correctly set to the latest
522        // event in the thread, which the timeline instance might or might
523        // not know about; in this case, we do a best effort of filling it, and resort
524        // to using the thread root if we don't know about any event.
525        //
526        // Note: we could trigger a back-pagination if the timeline is empty, and wait
527        // for the results, if the timeline is too often empty.
528
529        let latest_event_id = self
530            .controller
531            .items()
532            .await
533            .iter()
534            .rev()
535            .find_map(|item| {
536                if let TimelineItemKind::Event(event) = item.kind() {
537                    event.event_id().map(ToOwned::to_owned)
538                } else {
539                    None
540                }
541            })
542            .unwrap_or(thread_root);
543
544        Some(Reply {
545            event_id: latest_event_id,
546            enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
547            add_mentions: AddMentions::Yes,
548        })
549    }
550
551    /// Edit an event given its [`TimelineEventItemId`] and some new content.
552    ///
553    /// Only supports events for which [`EventTimelineItem::is_editable()`]
554    /// returns `true`.
555    #[instrument(skip(self, new_content))]
556    pub async fn edit(
557        &self,
558        item_id: &TimelineEventItemId,
559        new_content: EditedContent,
560    ) -> Result<(), Error> {
561        let items = self.items().await;
562        let Some((_pos, item)) = rfind_event_by_item_id(&items, item_id) else {
563            return Err(Error::EventNotInTimeline(item_id.clone()));
564        };
565
566        match item.handle() {
567            TimelineItemHandle::Remote(event_id) => {
568                let content = self
569                    .room()
570                    .make_edit_event(event_id, new_content)
571                    .await
572                    .map_err(EditError::RoomError)?;
573                self.send(content).await?;
574                Ok(())
575            }
576
577            TimelineItemHandle::Local(handle) => {
578                let new_content: AnyMessageLikeEventContent = match new_content {
579                    EditedContent::RoomMessage(message) => {
580                        if item.content.is_message() {
581                            // The replacement becomes the pending event itself, so restore its
582                            // relations, which the payload can't carry by type.
583                            AnyMessageLikeEventContent::RoomMessage(
584                                message.with_relation(item.content.relation()),
585                            )
586                        } else {
587                            return Err(EditError::ContentMismatch {
588                                original: item.content.debug_string().to_owned(),
589                                new: "a message".to_owned(),
590                            }
591                            .into());
592                        }
593                    }
594
595                    EditedContent::PollStart { new_content, .. } => {
596                        if item.content.is_poll() {
597                            AnyMessageLikeEventContent::UnstablePollStart(
598                                UnstablePollStartEventContent::New(
599                                    NewUnstablePollStartEventContent::new(new_content),
600                                ),
601                            )
602                        } else {
603                            return Err(EditError::ContentMismatch {
604                                original: item.content.debug_string().to_owned(),
605                                new: "a poll".to_owned(),
606                            }
607                            .into());
608                        }
609                    }
610
611                    EditedContent::MediaCaption { caption, formatted_caption, mentions } => {
612                        if handle
613                            .edit_media_caption(caption, formatted_caption, mentions)
614                            .await
615                            .map_err(RoomSendQueueError::StorageError)?
616                        {
617                            return Ok(());
618                        }
619                        return Err(EditError::InvalidLocalEchoState.into());
620                    }
621                };
622
623                if !handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)? {
624                    return Err(EditError::InvalidLocalEchoState.into());
625                }
626
627                Ok(())
628            }
629        }
630    }
631
632    /// Toggle a reaction on an event.
633    ///
634    /// Adds or redacts a reaction based on the state of the reaction at the
635    /// time it is called.
636    ///
637    /// When redacting a previous reaction, the redaction reason is not set.
638    ///
639    /// Ensures that only one reaction is sent at a time to avoid race
640    /// conditions and spamming the homeserver with requests.
641    ///
642    /// Returns `true` if the reaction was added, `false` if it was removed.
643    pub async fn toggle_reaction(
644        &self,
645        item_id: &TimelineEventItemId,
646        reaction_key: &str,
647    ) -> Result<bool, Error> {
648        self.controller.toggle_reaction_local(item_id, reaction_key, None).await
649    }
650
651    /// Same as [`Timeline::toggle_reaction`], merging `extra_content`'s fields
652    /// into the reaction's content when one is added.
653    ///
654    /// The reaction's own fields take precedence on conflicts. Removing a
655    /// reaction is a redaction, which carries no content, so `extra_content` is
656    /// only used when adding one — and only for reactions to remote events,
657    /// since a local echo is the user's own not-yet-sent event.
658    pub async fn toggle_reaction_with_extra_content(
659        &self,
660        item_id: &TimelineEventItemId,
661        reaction_key: &str,
662        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
663    ) -> Result<bool, Error> {
664        self.controller.toggle_reaction_local(item_id, reaction_key, extra_content).await
665    }
666
667    /// Sends an attachment to the room.
668    ///
669    /// It does not currently support local echoes.
670    ///
671    /// If the encryption feature is enabled, this method will transparently
672    /// encrypt the room message if the room is encrypted.
673    ///
674    /// The attachment and its optional thumbnail are stored in the media cache
675    /// and can be retrieved at any time, by calling
676    /// [`Media::get_media_content()`] with the `MediaSource` that can be found
677    /// in the corresponding `TimelineEventItem`, and using a
678    /// `MediaFormat::File`.
679    ///
680    /// # Arguments
681    ///
682    /// * `source` - The source of the attachment to send.
683    ///
684    /// * `mime_type` - The attachment's mime type.
685    ///
686    /// * `config` - An attachment configuration object containing details about
687    ///   the attachment like a thumbnail, its size, duration etc.
688    ///
689    /// [`Media::get_media_content()`]: matrix_sdk::Media::get_media_content
690    #[instrument(skip_all)]
691    pub fn send_attachment(
692        &self,
693        source: impl Into<AttachmentSource>,
694        mime_type: Mime,
695        config: AttachmentConfig,
696    ) -> SendAttachment<'_> {
697        SendAttachment::new(self, source.into(), mime_type, config)
698    }
699
700    /// Sends a media gallery to the room.
701    ///
702    /// If the encryption feature is enabled, this method will transparently
703    /// encrypt the room message if the room is encrypted.
704    ///
705    /// The attachments and their optional thumbnails are stored in the media
706    /// cache and can be retrieved at any time, by calling
707    /// [`Media::get_media_content()`] with the `MediaSource` that can be found
708    /// in the corresponding `TimelineEventItem`, and using a
709    /// `MediaFormat::File`.
710    ///
711    /// # Arguments
712    /// * `gallery` - A configuration object containing details about the
713    ///   gallery like files, thumbnails, etc.
714    ///
715    /// [`Media::get_media_content()`]: matrix_sdk::Media::get_media_content
716    #[cfg(feature = "unstable-msc4274")]
717    #[instrument(skip_all)]
718    pub fn send_gallery(&self, gallery: GalleryConfig) -> SendGallery<'_> {
719        SendGallery::new(self, gallery)
720    }
721
722    /// Redact an event given its [`TimelineEventItemId`] and an optional
723    /// reason.
724    pub async fn redact(
725        &self,
726        item_id: &TimelineEventItemId,
727        reason: Option<&str>,
728    ) -> Result<(), Error> {
729        let items = self.items().await;
730        let Some((_pos, event)) = rfind_event_by_item_id(&items, item_id) else {
731            return Err(RedactError::ItemNotFound(item_id.clone()).into());
732        };
733
734        match event.handle() {
735            TimelineItemHandle::Remote(event_id) => {
736                self.room()
737                    .send_queue()
738                    .redact(event_id.to_owned(), reason)
739                    .await
740                    .map_err(|_| Error::FailedSendingRedaction)?;
741                Ok(())
742            }
743            TimelineItemHandle::Local(handle) => {
744                // Forward the reason: if the local echo was being sent and the send wins the
745                // race, the server-side redaction that materializes the abort carries it.
746                if !handle
747                    .abort_with_reason(reason.map(ToOwned::to_owned))
748                    .await
749                    .map_err(RoomSendQueueError::StorageError)?
750                {
751                    return Err(RedactError::InvalidLocalEchoState.into());
752                }
753                Ok(())
754            }
755        }
756    }
757
758    /// Fetch unavailable details about the event with the given ID.
759    ///
760    /// This method only works for IDs of remote [`EventTimelineItem`]s,
761    /// to prevent losing details when a local echo is replaced by its
762    /// remote echo.
763    ///
764    /// This method tries to make all the requests it can. If an error is
765    /// encountered for a given request, it is forwarded with the
766    /// [`TimelineDetails::Error`] variant.
767    ///
768    /// # Arguments
769    ///
770    /// * `event_id` - The event ID of the event to fetch details for.
771    ///
772    /// # Errors
773    ///
774    /// Returns an error if the identifier doesn't match any event with a remote
775    /// echo in the timeline, or if the event is removed from the timeline
776    /// before all requests are handled.
777    #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
778    pub async fn fetch_details_for_event(&self, event_id: &EventId) -> Result<(), Error> {
779        self.controller.fetch_in_reply_to_details(event_id).await
780    }
781
782    /// Fetch all member events for the room this timeline is displaying.
783    ///
784    /// If the full member list is not known, sender profiles are currently
785    /// likely not going to be available. This will be fixed in the future.
786    ///
787    /// If fetching the members fails, any affected timeline items will have
788    /// the `sender_profile` set to [`TimelineDetails::Error`].
789    #[instrument(skip_all)]
790    pub async fn fetch_members(&self) {
791        self.controller.set_sender_profiles_pending().await;
792        match self.room().sync_members().await {
793            Ok(_) => {
794                self.controller.update_missing_sender_profiles().await;
795            }
796            Err(e) => {
797                self.controller.set_sender_profiles_error(Arc::new(e)).await;
798            }
799        }
800    }
801
802    /// Get the latest read receipt for the given user.
803    ///
804    /// Contrary to [`Room::load_user_receipt()`] that only keeps track of read
805    /// receipts received from the homeserver, this keeps also track of implicit
806    /// read receipts in this timeline, i.e. when a room member sends an event.
807    #[instrument(skip(self))]
808    pub async fn latest_user_read_receipt(
809        &self,
810        user_id: &UserId,
811    ) -> Option<(OwnedEventId, Receipt)> {
812        self.controller.latest_user_read_receipt(user_id).await
813    }
814
815    /// Get the ID of the timeline event with the latest read receipt for the
816    /// given user.
817    ///
818    /// In contrary to [`Self::latest_user_read_receipt()`], this allows to know
819    /// the position of the read receipt in the timeline even if the event it
820    /// applies to is not visible in the timeline, unless the event is unknown
821    /// by this timeline.
822    #[instrument(skip(self))]
823    pub async fn latest_user_read_receipt_timeline_event_id(
824        &self,
825        user_id: &UserId,
826    ) -> Option<OwnedEventId> {
827        self.controller.latest_user_read_receipt_timeline_event_id(user_id).await
828    }
829
830    /// Subscribe to changes in the read receipts of our own user.
831    pub async fn subscribe_own_user_read_receipts_changed(&self) -> impl Stream<Item = ()> + use<> {
832        self.controller.subscribe_own_user_read_receipts_changed().await
833    }
834
835    /// Send the given receipt.
836    ///
837    /// This uses [`Room::send_single_receipt`] internally, but checks
838    /// first if the receipt points to an event in this timeline that is more
839    /// recent than the current ones, to avoid unnecessary requests.
840    ///
841    /// If an unthreaded receipt is sent, this will also unset the unread flag
842    /// of the room if necessary.
843    ///
844    /// The thread of the receipt is determined by the timeline instance's
845    /// focus mode and `hide_threaded_events` flag.
846    ///
847    /// Returns a boolean indicating if it sent the receipt or not.
848    #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
849    pub async fn send_single_receipt(
850        &self,
851        receipt_type: ReceiptType,
852        event_id: OwnedEventId,
853    ) -> Result<bool> {
854        self.send_single_receipt_inner(receipt_type, event_id, false).await
855    }
856
857    /// Same as [`Self::send_single_receipt`], but lets the caller state whether
858    /// this is part of marking the whole room as read.
859    ///
860    /// When it is, and the only candidate event is one of the user's own, a
861    /// receipt is still sent against it so the homeserver recomputes its
862    /// push/badge count. See [`TimelineController::should_send_receipt`].
863    async fn send_single_receipt_inner(
864        &self,
865        receipt_type: ReceiptType,
866        event_id: OwnedEventId,
867        is_marking_room_as_read: bool,
868    ) -> Result<bool> {
869        let thread = self.controller.infer_thread_for_read_receipt(&receipt_type);
870
871        let event_id = match self
872            .controller
873            .should_send_receipt(&receipt_type, &thread, &event_id, is_marking_room_as_read)
874            .await
875        {
876            SendReceiptDecision::SendTo(event_id) => event_id,
877            SendReceiptDecision::DoNotSend => {
878                trace!("not sending receipt, because it wouldn't move the real receipt forwards");
879
880                if thread == ReceiptThread::Unthreaded {
881                    // Unset the read marker.
882                    self.room().set_unread_flag(false).await?;
883                }
884
885                return Ok(false);
886            }
887        };
888
889        trace!("sending receipt");
890        self.room().send_single_receipt(receipt_type, thread, event_id).await?;
891        Ok(true)
892    }
893
894    /// Send the given receipts.
895    ///
896    /// This uses [`Room::send_multiple_receipts`] internally, but
897    /// checks first if the receipts point to events in this timeline that
898    /// are more recent than the current ones, to avoid unnecessary
899    /// requests.
900    ///
901    /// This also unsets the unread marker of the room if necessary.
902    #[instrument(skip(self))]
903    pub async fn send_multiple_receipts(&self, mut receipts: Receipts) -> Result<()> {
904        if let Some(fully_read) = &receipts.fully_read {
905            receipts.fully_read = match self
906                .controller
907                .should_send_receipt(
908                    &ReceiptType::FullyRead,
909                    &ReceiptThread::Unthreaded,
910                    fully_read,
911                    false,
912                )
913                .await
914            {
915                SendReceiptDecision::SendTo(event_id) => Some(event_id),
916                SendReceiptDecision::DoNotSend => None,
917            };
918        }
919
920        if let Some(read_receipt) = &receipts.public_read_receipt {
921            receipts.public_read_receipt = match self
922                .controller
923                .should_send_receipt(
924                    &ReceiptType::Read,
925                    &ReceiptThread::Unthreaded,
926                    read_receipt,
927                    false,
928                )
929                .await
930            {
931                SendReceiptDecision::SendTo(event_id) => Some(event_id),
932                SendReceiptDecision::DoNotSend => None,
933            };
934        }
935
936        if let Some(private_read_receipt) = &receipts.private_read_receipt {
937            receipts.private_read_receipt = match self
938                .controller
939                .should_send_receipt(
940                    &ReceiptType::ReadPrivate,
941                    &ReceiptThread::Unthreaded,
942                    private_read_receipt,
943                    false,
944                )
945                .await
946            {
947                SendReceiptDecision::SendTo(event_id) => Some(event_id),
948                SendReceiptDecision::DoNotSend => None,
949            };
950        }
951
952        let room = self.room();
953
954        if !receipts.is_empty() {
955            room.send_multiple_receipts(receipts).await?;
956        } else {
957            room.set_unread_flag(false).await?;
958        }
959
960        Ok(())
961    }
962
963    /// Mark the timeline as read by attempting to send a read receipt on the
964    /// latest visible event.
965    ///
966    /// The latest visible event is determined from the timeline's focus kind
967    /// and whether or not it hides threaded events. If no latest event can
968    /// be determined and the timeline is live, the room's unread marker is
969    /// unset instead.
970    ///
971    /// # Arguments
972    ///
973    /// * `receipt_type` - The type of receipt to send. When using
974    ///   [`ReceiptType::FullyRead`], an unthreaded receipt will be sent. This
975    ///   works even if the latest event belongs to a thread, as a threaded
976    ///   reply also belongs to the unthreaded timeline. Otherwise the
977    ///   [`ReceiptThread`] will be determined based on the timeline's focus
978    ///   kind.
979    ///
980    /// # Returns
981    ///
982    /// A boolean indicating if the receipt was sent or not.
983    #[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
984    pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<bool> {
985        if let Some(event_id) = self.controller.latest_event_id().await {
986            self.send_single_receipt_inner(receipt_type, event_id, true).await
987        } else {
988            trace!("can't mark room as read because there's no latest event id");
989
990            // For live timelines, unset the read marker in this case.
991            if self.controller.is_live() {
992                self.room().set_unread_flag(false).await?;
993            }
994
995            Ok(false)
996        }
997    }
998
999    /// Create a [`EmbeddedEvent`] from an arbitrary event, be it in the
1000    /// timeline or not.
1001    ///
1002    /// Can be `None` if the event cannot be represented as a standalone item,
1003    /// because it's an aggregation.
1004    pub async fn make_replied_to(
1005        &self,
1006        event: TimelineEvent,
1007    ) -> Result<Option<EmbeddedEvent>, Error> {
1008        self.controller.make_replied_to(event).await
1009    }
1010
1011    /// Returns whether this timeline is focused on a thread (be it live, or
1012    /// from a permalink to a threaded event).
1013    pub fn is_threaded(&self) -> bool {
1014        self.controller.is_threaded()
1015    }
1016}
1017
1018/// Test helpers, likely not very useful in production.
1019#[doc(hidden)]
1020impl Timeline {
1021    /// Get the current list of timeline items.
1022    pub async fn items(&self) -> Vector<Arc<TimelineItem>> {
1023        self.controller.items().await
1024    }
1025
1026    pub async fn subscribe_filter_map<U: Clone>(
1027        &self,
1028        f: impl Fn(Arc<TimelineItem>) -> Option<U>,
1029    ) -> (Vector<U>, impl Stream<Item = VectorDiff<U>>) {
1030        let (items, stream) = self.controller.subscribe_filter_map(f).await;
1031        let stream = TimelineWithDropHandle::new(stream, self.drop_handle.clone());
1032        (items, stream)
1033    }
1034}
1035
1036#[derive(Debug)]
1037struct TimelineDropHandle {
1038    _room_update_join_handle: BackgroundTaskHandle,
1039    #[cfg(feature = "unstable-msc4426")]
1040    _global_profile_updates_handle: BackgroundTaskHandle,
1041    _local_echo_listener_handle: BackgroundTaskHandle,
1042    _rtc_membership_listener_handle: BackgroundTaskHandle,
1043    _event_cache_drop_handle: Arc<EventCacheDropHandles>,
1044    _focus_drop_handle: Option<BackgroundTaskHandle>,
1045    _crypto_drop_handles: CryptoDropHandles,
1046}
1047
1048#[cfg(not(target_family = "wasm"))]
1049pub type TimelineEventFilterFn =
1050    dyn Fn(&AnySyncTimelineEvent, &RoomVersionRules) -> bool + Send + Sync;
1051#[cfg(target_family = "wasm")]
1052pub type TimelineEventFilterFn = dyn Fn(&AnySyncTimelineEvent, &RoomVersionRules) -> bool;
1053
1054/// A source for sending an attachment.
1055///
1056/// The [`AttachmentSource::File`] variant can be constructed from any type that
1057/// implements `Into<PathBuf>`.
1058#[derive(Debug, Clone)]
1059pub enum AttachmentSource {
1060    /// The data of the attachment.
1061    Data {
1062        /// The bytes of the attachment.
1063        bytes: Vec<u8>,
1064
1065        /// The filename of the attachment.
1066        filename: String,
1067    },
1068
1069    /// An attachment loaded from a file.
1070    ///
1071    /// The bytes and the filename will be read from the file at the given path.
1072    File(PathBuf),
1073}
1074
1075impl AttachmentSource {
1076    /// Try to convert this attachment source into a `(bytes, filename)` tuple.
1077    pub(crate) fn try_into_bytes_and_filename(self) -> Result<(Vec<u8>, String), Error> {
1078        match self {
1079            Self::Data { bytes, filename } => Ok((bytes, filename)),
1080            Self::File(path) => {
1081                let filename = path
1082                    .file_name()
1083                    .ok_or(Error::InvalidAttachmentFileName)?
1084                    .to_str()
1085                    .ok_or(Error::InvalidAttachmentFileName)?
1086                    .to_owned();
1087                let bytes = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?;
1088                Ok((bytes, filename))
1089            }
1090        }
1091    }
1092}
1093
1094impl<P> From<P> for AttachmentSource
1095where
1096    P: Into<PathBuf>,
1097{
1098    fn from(value: P) -> Self {
1099        Self::File(value.into())
1100    }
1101}
1102
1103/// Configuration for sending a gallery.
1104///
1105/// This duplicates [`matrix_sdk::attachment::GalleryConfig`] but uses an
1106/// `AttachmentSource` so that we can delay loading the actual data until we're
1107/// inside the SendGallery future. This allows [`Timeline::send_gallery`] to
1108/// return early without blocking the caller.
1109#[cfg(feature = "unstable-msc4274")]
1110#[derive(Debug, Default)]
1111pub struct GalleryConfig {
1112    pub(crate) txn_id: Option<OwnedTransactionId>,
1113    pub(crate) items: Vec<GalleryItemInfo>,
1114    pub(crate) caption: Option<TextMessageEventContent>,
1115    pub(crate) mentions: Option<Mentions>,
1116    pub(crate) in_reply_to: Option<OwnedEventId>,
1117}
1118
1119#[cfg(feature = "unstable-msc4274")]
1120impl GalleryConfig {
1121    /// Create a new empty `GalleryConfig`.
1122    pub fn new() -> Self {
1123        Self::default()
1124    }
1125
1126    /// Set the transaction ID to send.
1127    ///
1128    /// # Arguments
1129    ///
1130    /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` held
1131    ///   in its unsigned field as `transaction_id`. If not given, one is
1132    ///   created for the message.
1133    #[must_use]
1134    pub fn txn_id(mut self, txn_id: OwnedTransactionId) -> Self {
1135        self.txn_id = Some(txn_id);
1136        self
1137    }
1138
1139    /// Adds a media item to the gallery.
1140    ///
1141    /// # Arguments
1142    ///
1143    /// * `item` - Information about the item to be added.
1144    #[must_use]
1145    pub fn add_item(mut self, item: GalleryItemInfo) -> Self {
1146        self.items.push(item);
1147        self
1148    }
1149
1150    /// Set the optional caption.
1151    ///
1152    /// # Arguments
1153    ///
1154    /// * `caption` - The optional caption.
1155    pub fn caption(mut self, caption: Option<TextMessageEventContent>) -> Self {
1156        self.caption = caption;
1157        self
1158    }
1159
1160    /// Set the mentions of the message.
1161    ///
1162    /// # Arguments
1163    ///
1164    /// * `mentions` - The mentions of the message.
1165    pub fn mentions(mut self, mentions: Option<Mentions>) -> Self {
1166        self.mentions = mentions;
1167        self
1168    }
1169
1170    /// Set the reply information of the message.
1171    ///
1172    /// # Arguments
1173    ///
1174    /// * `event_id` - The event ID to reply to.
1175    pub fn in_reply_to(mut self, event_id: Option<OwnedEventId>) -> Self {
1176        self.in_reply_to = event_id;
1177        self
1178    }
1179
1180    /// Returns the number of media items in the gallery.
1181    pub fn len(&self) -> usize {
1182        self.items.len()
1183    }
1184
1185    /// Checks whether the gallery contains any media items or not.
1186    pub fn is_empty(&self) -> bool {
1187        self.items.is_empty()
1188    }
1189}
1190
1191#[cfg(feature = "unstable-msc4274")]
1192#[derive(Debug)]
1193/// Metadata for a gallery item
1194pub struct GalleryItemInfo {
1195    /// The attachment source.
1196    pub source: AttachmentSource,
1197    /// The mime type.
1198    pub content_type: Mime,
1199    /// The attachment info.
1200    pub attachment_info: AttachmentInfo,
1201    /// The caption.
1202    pub caption: Option<TextMessageEventContent>,
1203    /// The thumbnail.
1204    pub thumbnail: Option<Thumbnail>,
1205}
1206
1207#[cfg(feature = "unstable-msc4274")]
1208impl TryFrom<GalleryItemInfo> for matrix_sdk::attachment::GalleryItemInfo {
1209    type Error = Error;
1210
1211    fn try_from(value: GalleryItemInfo) -> Result<Self, Self::Error> {
1212        let (data, filename) = value.source.try_into_bytes_and_filename()?;
1213        Ok(matrix_sdk::attachment::GalleryItemInfo {
1214            filename,
1215            content_type: value.content_type,
1216            data,
1217            attachment_info: value.attachment_info,
1218            caption: value.caption,
1219            thumbnail: value.thumbnail,
1220        })
1221    }
1222}
1223
1224#[derive(Clone, Debug)]
1225#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1226/// The level of read receipt tracking for the timeline.
1227pub enum TimelineReadReceiptTracking {
1228    /// Track read receipts for all events.
1229    AllEvents,
1230    /// Track read receipts only for message-like events.
1231    MessageLikeEvents,
1232    /// Disable read receipt tracking.
1233    Disabled,
1234}
1235
1236impl TimelineReadReceiptTracking {
1237    /// Whether or not read receipt tracking is enabled.
1238    pub fn is_enabled(&self) -> bool {
1239        match self {
1240            TimelineReadReceiptTracking::AllEvents
1241            | TimelineReadReceiptTracking::MessageLikeEvents => true,
1242            TimelineReadReceiptTracking::Disabled => false,
1243        }
1244    }
1245}