Skip to main content

matrix_sdk_ui/timeline/
mod.rs

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