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