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