matrix_sdk_ffi/timeline/
mod.rs

1// Copyright 2023 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
15use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
16
17use anyhow::{Context, Result};
18use eyeball_im::VectorDiff;
19use futures_util::pin_mut;
20use matrix_sdk::{
21    attachment::{
22        AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
23    },
24    deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
25    event_cache::RoomPaginationStatus,
26    room::edit::EditedContent as SdkEditedContent,
27};
28use matrix_sdk_common::{
29    executor::{AbortHandle, JoinHandle},
30    stream::StreamExt,
31};
32use matrix_sdk_ui::timeline::{
33    self, AttachmentConfig, AttachmentSource, EventItemOrigin,
34    LatestEventValue as UiLatestEventValue, MediaUploadProgress as SdkMediaUploadProgress, Profile,
35    TimelineDetails, TimelineUniqueId as SdkTimelineUniqueId,
36};
37use mime::Mime;
38use reply::{EmbeddedEventDetails, InReplyToDetails};
39use ruma::{
40    assign,
41    events::{
42        location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
43        poll::{
44            unstable_end::UnstablePollEndEventContent,
45            unstable_response::UnstablePollResponseEventContent,
46            unstable_start::{
47                NewUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollAnswers,
48                UnstablePollStartContentBlock,
49            },
50        },
51        room::message::{
52            LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
53            TextMessageEventContent,
54        },
55        AnyMessageLikeEventContent,
56    },
57    EventId, UInt,
58};
59use tokio::sync::Mutex;
60use tracing::{error, warn};
61use uuid::Uuid;
62
63use self::content::TimelineItemContent;
64pub use self::msg_like::MessageContent;
65use crate::{
66    error::{ClientError, RoomError},
67    event::EventOrTransactionId,
68    ruma::{
69        AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
70        ThumbnailInfo, VideoInfo,
71    },
72    runtime::get_runtime_handle,
73    task_handle::TaskHandle,
74    utils::Timestamp,
75};
76
77pub mod configuration;
78mod content;
79mod msg_like;
80mod reply;
81
82use matrix_sdk::utils::formatted_body_from;
83use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
84
85use crate::error::QueueWedgeError;
86
87#[derive(uniffi::Object)]
88#[repr(transparent)]
89pub struct Timeline {
90    pub(crate) inner: matrix_sdk_ui::timeline::Timeline,
91}
92
93impl Timeline {
94    pub(crate) fn new(inner: matrix_sdk_ui::timeline::Timeline) -> Arc<Self> {
95        Arc::new(Self { inner })
96    }
97
98    fn send_attachment(
99        self: Arc<Self>,
100        params: UploadParameters,
101        attachment_info: AttachmentInfo,
102        mime_type: Option<String>,
103        thumbnail: Option<Thumbnail>,
104    ) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
105        let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
106
107        let mime_type =
108            mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
109
110        let in_reply_to_event_id = params
111            .in_reply_to
112            .map(EventId::parse)
113            .transpose()
114            .map_err(|_| RoomError::InvalidRepliedToEventId)?;
115
116        let caption = params.caption.map(|caption| {
117            let formatted =
118                formatted_body_from(Some(&caption), params.formatted_caption.map(Into::into));
119            assign!(TextMessageEventContent::plain(caption), { formatted })
120        });
121
122        let attachment_config = AttachmentConfig {
123            info: Some(attachment_info),
124            thumbnail,
125            caption,
126            mentions: params.mentions.map(Into::into),
127            in_reply_to: in_reply_to_event_id,
128            ..Default::default()
129        };
130
131        let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
132            self.inner
133                .send_attachment(params.source, mime_type, attachment_config)
134                .use_send_queue()
135                .await
136                .map_err(|_| RoomError::FailedSendingAttachment)
137        }));
138
139        Ok(handle)
140    }
141}
142
143fn build_thumbnail_info(
144    thumbnail_source: Option<UploadSource>,
145    thumbnail_info: Option<ThumbnailInfo>,
146) -> Result<Option<Thumbnail>, RoomError> {
147    match (thumbnail_source, thumbnail_info) {
148        (None, None) => Ok(None),
149
150        (Some(thumbnail_source), Some(thumbnail_info)) => {
151            let thumbnail_data = match thumbnail_source {
152                UploadSource::File { filename } => {
153                    fs::read(filename).map_err(|_| RoomError::InvalidThumbnailData)?
154                }
155                UploadSource::Data { bytes, .. } => bytes,
156            };
157
158            let height = thumbnail_info
159                .height
160                .and_then(|u| UInt::try_from(u).ok())
161                .ok_or(RoomError::InvalidAttachmentData)?;
162            let width = thumbnail_info
163                .width
164                .and_then(|u| UInt::try_from(u).ok())
165                .ok_or(RoomError::InvalidAttachmentData)?;
166            let size = thumbnail_info
167                .size
168                .and_then(|u| UInt::try_from(u).ok())
169                .ok_or(RoomError::InvalidAttachmentData)?;
170
171            let mime_str =
172                thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
173            let mime_type =
174                mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
175
176            Ok(Some(Thumbnail {
177                data: thumbnail_data,
178                content_type: mime_type,
179                height,
180                width,
181                size,
182            }))
183        }
184
185        _ => {
186            warn!("Ignoring thumbnail because either the thumbnail source or info isn't defined");
187            Ok(None)
188        }
189    }
190}
191
192#[derive(uniffi::Record)]
193pub struct UploadParameters {
194    /// Source from which to upload data
195    source: UploadSource,
196    /// Optional non-formatted caption, for clients that support it.
197    caption: Option<String>,
198    /// Optional HTML-formatted caption, for clients that support it.
199    formatted_caption: Option<FormattedBody>,
200    /// Optional intentional mentions to be sent with the media.
201    mentions: Option<Mentions>,
202    /// Optional Event ID to reply to.
203    in_reply_to: Option<String>,
204}
205
206/// A source for uploading a file
207#[derive(Clone, uniffi::Enum)]
208pub enum UploadSource {
209    /// Upload source is a file on disk
210    File {
211        /// Path to file
212        filename: String,
213    },
214    /// Upload source is data in memory
215    Data {
216        /// Bytes being uploaded
217        bytes: Vec<u8>,
218        /// Filename to associate with bytes
219        filename: String,
220    },
221}
222
223impl From<UploadSource> for AttachmentSource {
224    fn from(value: UploadSource) -> Self {
225        match value {
226            UploadSource::File { filename } => Self::File(filename.into()),
227            UploadSource::Data { bytes, filename } => Self::Data { bytes, filename },
228        }
229    }
230}
231
232/// This type represents the progress of a media (consisting of a file and
233/// possibly a thumbnail) being uploaded.
234#[derive(Clone, Copy, uniffi::Record)]
235pub struct MediaUploadProgress {
236    /// The index of the media within the transaction. A file and its
237    /// thumbnail share the same index. Will always be 0 for non-gallery
238    /// media uploads.
239    pub index: u64,
240
241    /// The current combined upload progress for both the file and,
242    /// if it exists, its thumbnail.
243    pub progress: AbstractProgress,
244}
245
246impl From<SdkMediaUploadProgress> for MediaUploadProgress {
247    fn from(value: SdkMediaUploadProgress) -> Self {
248        Self { index: value.index, progress: value.progress.into() }
249    }
250}
251
252/// Progress of an operation in abstract units.
253///
254/// Contrary to [`TransmissionProgress`], this allows tracking the progress
255/// of sending or receiving a payload in estimated pseudo units representing a
256/// percentage. This is helpful in cases where the exact progress in bytes isn't
257/// known, for instance, because encryption (which changes the size) happens on
258/// the fly.
259#[derive(Clone, Copy, uniffi::Record)]
260pub struct AbstractProgress {
261    /// How many units were already transferred.
262    pub current: u64,
263    /// How many units there are in total.
264    pub total: u64,
265}
266
267impl From<matrix_sdk::send_queue::AbstractProgress> for AbstractProgress {
268    fn from(value: matrix_sdk::send_queue::AbstractProgress) -> Self {
269        Self {
270            current: value.current.try_into().unwrap_or(u64::MAX),
271            total: value.total.try_into().unwrap_or(u64::MAX),
272        }
273    }
274}
275
276#[matrix_sdk_ffi_macros::export]
277impl Timeline {
278    pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {
279        let (timeline_items, timeline_stream) = self.inner.subscribe().await;
280
281        // It's important that the initial items are passed *before* we forward the
282        // stream updates, with a guaranteed ordering. Otherwise, it could
283        // be that the listener be called before the initial items have been
284        // handled by the caller. See #3535 for details.
285
286        // First, pass all the items as a reset update.
287        listener.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
288
289        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
290            pin_mut!(timeline_stream);
291
292            // Then forward new items.
293            while let Some(diffs) = timeline_stream.next().await {
294                listener.on_update(diffs.into_iter().map(TimelineDiff::new).collect());
295            }
296        })))
297    }
298
299    pub fn retry_decryption(self: Arc<Self>, session_ids: Vec<String>) {
300        get_runtime_handle().spawn(async move {
301            self.inner.retry_decryption(&session_ids).await;
302        });
303    }
304
305    pub async fn fetch_members(&self) {
306        self.inner.fetch_members().await
307    }
308
309    pub async fn subscribe_to_back_pagination_status(
310        &self,
311        listener: Box<dyn PaginationStatusListener>,
312    ) -> Result<Arc<TaskHandle>, ClientError> {
313        let (initial, mut subscriber) = self
314            .inner
315            .live_back_pagination_status()
316            .await
317            .context("can't subscribe to the back-pagination status on a focused timeline")?;
318
319        // Send the current state even if it hasn't changed right away.
320        //
321        // Note: don't do it in the spawned function, so that the caller is immediately
322        // aware of the current state, and this doesn't depend on the async runtime
323        // having an available worker
324        listener.on_update(initial);
325
326        Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
327            while let Some(status) = subscriber.next().await {
328                listener.on_update(status);
329            }
330        }))))
331    }
332
333    /// Paginate backwards, whether we are in focused mode or in live mode.
334    ///
335    /// Returns whether we hit the start of the timeline or not.
336    pub async fn paginate_backwards(&self, num_events: u16) -> Result<bool, ClientError> {
337        Ok(self.inner.paginate_backwards(num_events).await?)
338    }
339
340    /// Paginate forwards, whether we are in focused mode or in live mode.
341    ///
342    /// Returns whether we hit the end of the timeline or not.
343    pub async fn paginate_forwards(&self, num_events: u16) -> Result<bool, ClientError> {
344        Ok(self.inner.paginate_forwards(num_events).await?)
345    }
346
347    pub async fn send_read_receipt(
348        &self,
349        receipt_type: ReceiptType,
350        event_id: String,
351    ) -> Result<(), ClientError> {
352        let event_id = EventId::parse(event_id)?;
353        self.inner.send_single_receipt(receipt_type.into(), event_id).await?;
354        Ok(())
355    }
356
357    /// Mark the timeline as read by attempting to send a read receipt on the
358    /// latest visible event.
359    ///
360    /// The latest visible event is determined from the timeline's focus kind
361    /// and whether or not it hides threaded events. If no latest event can
362    /// be determined and the timeline is live, the room's unread marker is
363    /// unset instead.
364    ///
365    /// # Arguments
366    ///
367    /// * `receipt_type` - The type of receipt to send. When using
368    ///   [`ReceiptType::FullyRead`], an unthreaded receipt will be sent. This
369    ///   works even if the latest event belongs to a thread, as a threaded
370    ///   reply also belongs to the unthreaded timeline. Otherwise the receipt
371    ///   thread will be determined based on the timeline's focus kind.
372    pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
373        self.inner.mark_as_read(receipt_type.into()).await?;
374        Ok(())
375    }
376
377    /// Returns the latest [`EventId`] in the timeline.
378    pub async fn latest_event_id(&self) -> Option<String> {
379        self.inner.latest_event_id().await.as_deref().map(ToString::to_string)
380    }
381
382    /// Queues an event in the room's send queue so it's processed for
383    /// sending later.
384    ///
385    /// Returns an abort handle that allows to abort sending, if it hasn't
386    /// happened yet.
387    pub async fn send(
388        self: Arc<Self>,
389        msg: Arc<RoomMessageEventContentWithoutRelation>,
390    ) -> Result<Arc<SendHandle>, ClientError> {
391        match self.inner.send((*msg).to_owned().with_relation(None).into()).await {
392            Ok(handle) => Ok(Arc::new(SendHandle::new(handle))),
393            Err(err) => {
394                error!("error when sending a message: {err}");
395                Err(err.into())
396            }
397        }
398    }
399
400    pub fn send_image(
401        self: Arc<Self>,
402        params: UploadParameters,
403        thumbnail_source: Option<UploadSource>,
404        image_info: ImageInfo,
405    ) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
406        let attachment_info = AttachmentInfo::Image(
407            BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?,
408        );
409        let thumbnail = build_thumbnail_info(thumbnail_source, image_info.thumbnail_info)?;
410        self.send_attachment(params, attachment_info, image_info.mimetype, thumbnail)
411    }
412
413    pub fn send_video(
414        self: Arc<Self>,
415        params: UploadParameters,
416        thumbnail_source: Option<UploadSource>,
417        video_info: VideoInfo,
418    ) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
419        let attachment_info = AttachmentInfo::Video(
420            BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?,
421        );
422        let thumbnail = build_thumbnail_info(thumbnail_source, video_info.thumbnail_info)?;
423        self.send_attachment(params, attachment_info, video_info.mimetype, thumbnail)
424    }
425
426    pub fn send_audio(
427        self: Arc<Self>,
428        params: UploadParameters,
429        audio_info: AudioInfo,
430    ) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
431        let attachment_info = AttachmentInfo::Audio(
432            BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?,
433        );
434        self.send_attachment(params, attachment_info, audio_info.mimetype, None)
435    }
436
437    pub fn send_voice_message(
438        self: Arc<Self>,
439        params: UploadParameters,
440        audio_info: AudioInfo,
441        waveform: Vec<f32>,
442    ) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
443        let mut info =
444            BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?;
445        info.waveform = Some(waveform);
446        self.send_attachment(params, AttachmentInfo::Voice(info), audio_info.mimetype, None)
447    }
448
449    pub fn send_file(
450        self: Arc<Self>,
451        params: UploadParameters,
452        file_info: FileInfo,
453    ) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
454        let attachment_info = AttachmentInfo::File(
455            BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?,
456        );
457        self.send_attachment(params, attachment_info, file_info.mimetype, None)
458    }
459
460    pub async fn create_poll(
461        self: Arc<Self>,
462        question: String,
463        answers: Vec<String>,
464        max_selections: u8,
465        poll_kind: PollKind,
466    ) -> Result<(), ClientError> {
467        let poll_data = PollData { question, answers, max_selections, poll_kind };
468
469        let poll_start_event_content = NewUnstablePollStartEventContent::plain_text(
470            poll_data.fallback_text(),
471            poll_data.try_into()?,
472        );
473        let event_content =
474            AnyMessageLikeEventContent::UnstablePollStart(poll_start_event_content.into());
475
476        if let Err(err) = self.inner.send(event_content).await {
477            error!("unable to start poll: {err}");
478        }
479
480        Ok(())
481    }
482
483    pub async fn send_poll_response(
484        self: Arc<Self>,
485        poll_start_event_id: String,
486        answers: Vec<String>,
487    ) -> Result<(), ClientError> {
488        let poll_start_event_id =
489            EventId::parse(poll_start_event_id).context("Failed to parse EventId")?;
490        let poll_response_event_content =
491            UnstablePollResponseEventContent::new(answers, poll_start_event_id);
492        let event_content =
493            AnyMessageLikeEventContent::UnstablePollResponse(poll_response_event_content);
494
495        if let Err(err) = self.inner.send(event_content).await {
496            error!("unable to send poll response: {err}");
497        }
498
499        Ok(())
500    }
501
502    pub async fn end_poll(
503        self: Arc<Self>,
504        poll_start_event_id: String,
505        text: String,
506    ) -> Result<(), ClientError> {
507        let poll_start_event_id =
508            EventId::parse(poll_start_event_id).context("Failed to parse EventId")?;
509        let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
510        let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
511
512        if let Err(err) = self.inner.send(event_content).await {
513            error!("unable to end poll: {err}");
514        }
515
516        Ok(())
517    }
518
519    /// Send a reply.
520    ///
521    /// If the replied to event has a thread relation, it is forwarded on the
522    /// reply so that clients that support threads can render the reply
523    /// inside the thread.
524    pub async fn send_reply(
525        &self,
526        msg: Arc<RoomMessageEventContentWithoutRelation>,
527        event_id: String,
528    ) -> Result<(), ClientError> {
529        let event_id = EventId::parse(&event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
530        self.inner.send_reply((*msg).clone(), event_id).await?;
531        Ok(())
532    }
533
534    /// Edits an event from the timeline.
535    ///
536    /// If it was a local event, this will *try* to edit it, if it was not
537    /// being sent already. If the event was a remote event, then it will be
538    /// redacted by sending an edit request to the server.
539    ///
540    /// Returns whether the edit did happen. It can only return false for
541    /// local events that are being processed.
542    pub async fn edit(
543        &self,
544        event_or_transaction_id: EventOrTransactionId,
545        new_content: EditedContent,
546    ) -> Result<(), ClientError> {
547        match self
548            .inner
549            .edit(&event_or_transaction_id.clone().try_into()?, new_content.clone().try_into()?)
550            .await
551        {
552            Ok(()) => Ok(()),
553
554            Err(timeline::Error::EventNotInTimeline(_)) => {
555                // If we couldn't edit, assume it was an (remote) event that wasn't in the
556                // timeline, and try to edit it via the room itself.
557                let event_id = match event_or_transaction_id {
558                    EventOrTransactionId::EventId { event_id } => EventId::parse(event_id)?,
559                    EventOrTransactionId::TransactionId { .. } => {
560                        warn!(
561                            "trying to apply an edit to a local echo that doesn't exist \
562                             in this timeline, aborting"
563                        );
564                        return Ok(());
565                    }
566                };
567                let room = self.inner.room();
568                let edit_event = room.make_edit_event(&event_id, new_content.try_into()?).await?;
569                room.send_queue().send(edit_event).await?;
570                Ok(())
571            }
572
573            Err(err) => Err(err.into()),
574        }
575    }
576
577    pub async fn send_location(
578        self: Arc<Self>,
579        body: String,
580        geo_uri: String,
581        description: Option<String>,
582        zoom_level: Option<u8>,
583        asset_type: Option<AssetType>,
584        replied_to_event_id: Option<String>,
585    ) -> Result<(), ClientError> {
586        let mut location_event_message_content =
587            LocationMessageEventContent::new(body, geo_uri.clone());
588
589        if let Some(asset_type) = asset_type {
590            location_event_message_content =
591                location_event_message_content.with_asset_type(RumaAssetType::from(asset_type));
592        }
593
594        let mut location_content = LocationContent::new(geo_uri);
595        location_content.description = description;
596        location_content.zoom_level = zoom_level.and_then(ZoomLevel::new);
597        location_event_message_content.location = Some(location_content);
598
599        let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
600            MessageType::Location(location_event_message_content),
601        );
602
603        if let Some(replied_to_event_id) = replied_to_event_id {
604            self.send_reply(Arc::new(room_message_event_content), replied_to_event_id).await
605        } else {
606            self.send(Arc::new(room_message_event_content)).await?;
607            Ok(())
608        }
609    }
610
611    /// Toggle a reaction on an event.
612    ///
613    /// Adds or redacts a reaction based on the state of the reaction at the
614    /// time it is called.
615    ///
616    /// This method works both on local echoes and remote items.
617    ///
618    /// When redacting a previous reaction, the redaction reason is not set.
619    ///
620    /// Ensures that only one reaction is sent at a time to avoid race
621    /// conditions and spamming the homeserver with requests.
622    ///
623    /// Returns `true` if the reaction was added, `false` if it was removed.
624    pub async fn toggle_reaction(
625        &self,
626        item_id: EventOrTransactionId,
627        key: String,
628    ) -> Result<bool, ClientError> {
629        Ok(self.inner.toggle_reaction(&item_id.try_into()?, &key).await?)
630    }
631
632    pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
633        let event_id = <&EventId>::try_from(event_id.as_str())?;
634        self.inner
635            .fetch_details_for_event(event_id)
636            .await
637            .map_err(|e| ClientError::from_str(e, Some("Fetching event details".to_owned())))?;
638        Ok(())
639    }
640
641    /// Get the current timeline item for the given event ID, if any.
642    ///
643    /// Will return a remote event, *or* a local echo that has been sent but not
644    /// yet replaced by a remote echo.
645    ///
646    /// It's preferable to store the timeline items in the model for your UI, if
647    /// possible, instead of just storing IDs and coming back to the timeline
648    /// object to look up items.
649    pub async fn get_event_timeline_item_by_event_id(
650        &self,
651        event_id: String,
652    ) -> Result<EventTimelineItem, ClientError> {
653        let event_id = EventId::parse(event_id)?;
654        let item = self
655            .inner
656            .item_by_event_id(&event_id)
657            .await
658            .context("Item with given event ID not found")?;
659        Ok(item.into())
660    }
661
662    /// Redacts an event from the timeline.
663    ///
664    /// Only works for events that exist as timeline items.
665    ///
666    /// If it was a local event, this will *try* to cancel it, if it was not
667    /// being sent already. If the event was a remote event, then it will be
668    /// redacted by sending a redaction request to the server.
669    ///
670    /// Will return an error if the event couldn't be redacted.
671    pub async fn redact_event(
672        &self,
673        event_or_transaction_id: EventOrTransactionId,
674        reason: Option<String>,
675    ) -> Result<(), ClientError> {
676        Ok(self.inner.redact(&(event_or_transaction_id.try_into()?), reason.as_deref()).await?)
677    }
678
679    /// Load the reply details for the given event id.
680    ///
681    /// This will return an `InReplyToDetails` object that contains the details
682    /// which will either be ready or an error.
683    pub async fn load_reply_details(
684        &self,
685        event_id_str: String,
686    ) -> Result<Arc<InReplyToDetails>, ClientError> {
687        let event_id = EventId::parse(&event_id_str)?;
688
689        let replied_to = match self.inner.room().load_or_fetch_event(&event_id, None).await {
690            Ok(event) => self.inner.make_replied_to(event).await.map_err(ClientError::from),
691            Err(e) => Err(ClientError::from(e)),
692        };
693
694        match replied_to {
695            Ok(Some(replied_to)) => Ok(Arc::new(InReplyToDetails::new(
696                event_id_str,
697                EmbeddedEventDetails::Ready {
698                    content: replied_to.content.clone().into(),
699                    sender: replied_to.sender.to_string(),
700                    sender_profile: replied_to.sender_profile.into(),
701                    timestamp: replied_to.timestamp.into(),
702                    event_or_transaction_id: replied_to.identifier.into(),
703                },
704            ))),
705
706            Ok(None) => Ok(Arc::new(InReplyToDetails::new(
707                event_id_str,
708                EmbeddedEventDetails::Error { message: "unsupported event".to_owned() },
709            ))),
710
711            Err(e) => Ok(Arc::new(InReplyToDetails::new(
712                event_id_str,
713                EmbeddedEventDetails::Error { message: e.to_string() },
714            ))),
715        }
716    }
717
718    /// Adds a new pinned event by sending an updated `m.room.pinned_events`
719    /// event containing the new event id.
720    ///
721    /// Returns `true` if we sent the request, `false` if the event was already
722    /// pinned.
723    async fn pin_event(&self, event_id: String) -> Result<bool, ClientError> {
724        let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
725        self.inner.pin_event(&event_id).await.map_err(ClientError::from)
726    }
727
728    /// Adds a new pinned event by sending an updated `m.room.pinned_events`
729    /// event without the event id we want to remove.
730    ///
731    /// Returns `true` if we sent the request, `false` if the event wasn't
732    /// pinned
733    async fn unpin_event(&self, event_id: String) -> Result<bool, ClientError> {
734        let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
735        self.inner.unpin_event(&event_id).await.map_err(ClientError::from)
736    }
737
738    pub fn create_message_content(
739        &self,
740        msg_type: crate::ruma::MessageType,
741    ) -> Option<Arc<RoomMessageEventContentWithoutRelation>> {
742        let msg_type: Option<MessageType> = msg_type.try_into().ok();
743        msg_type.map(|m| Arc::new(RoomMessageEventContentWithoutRelation::new(m)))
744    }
745}
746
747/// A handle to perform actions onto a local echo.
748#[derive(uniffi::Object)]
749pub struct SendHandle {
750    inner: Mutex<Option<matrix_sdk::send_queue::SendHandle>>,
751}
752
753impl SendHandle {
754    fn new(handle: matrix_sdk::send_queue::SendHandle) -> Self {
755        Self { inner: Mutex::new(Some(handle)) }
756    }
757}
758
759#[matrix_sdk_ffi_macros::export]
760impl SendHandle {
761    /// Try to abort the sending of the current event.
762    ///
763    /// If this returns `true`, then the sending could be aborted, because the
764    /// event hasn't been sent yet. Otherwise, if this returns `false`, the
765    /// event had already been sent and could not be aborted.
766    ///
767    /// This has an effect only on the first call; subsequent calls will always
768    /// return `false`.
769    async fn abort(self: Arc<Self>) -> Result<bool, ClientError> {
770        if let Some(inner) = self.inner.lock().await.take() {
771            Ok(inner
772                .abort()
773                .await
774                .map_err(|err| anyhow::anyhow!("error when saving in store: {err}"))?)
775        } else {
776            warn!("trying to abort a send handle that's already been actioned");
777            Ok(false)
778        }
779    }
780
781    /// Attempt to manually resend messages that failed to send due to issues
782    /// that should now have been fixed.
783    ///
784    /// This is useful for example, when there's a
785    /// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
786    /// the user may have re-verified on a different device and would now
787    /// like to send the failed message that's waiting on this device.
788    ///
789    /// # Arguments
790    ///
791    /// * `transaction_id` - The send queue transaction identifier of the local
792    ///   echo that should be unwedged.
793    pub async fn try_resend(self: Arc<Self>) -> Result<(), ClientError> {
794        let locked = self.inner.lock().await;
795        if let Some(handle) = locked.as_ref() {
796            handle.unwedge().await?;
797        } else {
798            warn!("trying to unwedge a send handle that's been aborted");
799        }
800        Ok(())
801    }
802}
803
804#[derive(Debug, thiserror::Error, uniffi::Error)]
805pub enum FocusEventError {
806    #[error("the event id parameter {event_id} is incorrect: {err}")]
807    InvalidEventId { event_id: String, err: String },
808
809    #[error("the event {event_id} could not be found")]
810    EventNotFound { event_id: String },
811
812    #[error("error when trying to focus on an event: {msg}")]
813    Other { msg: String },
814}
815
816#[matrix_sdk_ffi_macros::export(callback_interface)]
817pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
818    fn on_update(&self, diff: Vec<TimelineDiff>);
819}
820
821#[matrix_sdk_ffi_macros::export(callback_interface)]
822pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
823    fn on_update(&self, status: RoomPaginationStatus);
824}
825
826#[derive(Clone, uniffi::Enum)]
827pub enum TimelineDiff {
828    Append { values: Vec<Arc<TimelineItem>> },
829    Clear,
830    PushFront { value: Arc<TimelineItem> },
831    PushBack { value: Arc<TimelineItem> },
832    PopFront,
833    PopBack,
834    Insert { index: u32, value: Arc<TimelineItem> },
835    Set { index: u32, value: Arc<TimelineItem> },
836    Remove { index: u32 },
837    Truncate { length: u32 },
838    Reset { values: Vec<Arc<TimelineItem>> },
839}
840
841impl TimelineDiff {
842    pub(crate) fn new(inner: VectorDiff<Arc<matrix_sdk_ui::timeline::TimelineItem>>) -> Self {
843        match inner {
844            VectorDiff::Append { values } => {
845                Self::Append { values: values.into_iter().map(TimelineItem::from_arc).collect() }
846            }
847            VectorDiff::Clear => Self::Clear,
848            VectorDiff::Insert { index, value } => Self::Insert {
849                index: u32::try_from(index).unwrap(),
850                value: TimelineItem::from_arc(value),
851            },
852            VectorDiff::Set { index, value } => Self::Set {
853                index: u32::try_from(index).unwrap(),
854                value: TimelineItem::from_arc(value),
855            },
856            VectorDiff::Truncate { length } => {
857                Self::Truncate { length: u32::try_from(length).unwrap() }
858            }
859            VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
860            VectorDiff::PushBack { value } => {
861                Self::PushBack { value: TimelineItem::from_arc(value) }
862            }
863            VectorDiff::PushFront { value } => {
864                Self::PushFront { value: TimelineItem::from_arc(value) }
865            }
866            VectorDiff::PopBack => Self::PopBack,
867            VectorDiff::PopFront => Self::PopFront,
868            VectorDiff::Reset { values } => {
869                Self::Reset { values: values.into_iter().map(TimelineItem::from_arc).collect() }
870            }
871        }
872    }
873}
874
875#[derive(Clone, uniffi::Record)]
876pub struct TimelineUniqueId {
877    id: String,
878}
879
880impl From<&SdkTimelineUniqueId> for TimelineUniqueId {
881    fn from(value: &SdkTimelineUniqueId) -> Self {
882        Self { id: value.0.clone() }
883    }
884}
885
886impl From<&TimelineUniqueId> for SdkTimelineUniqueId {
887    fn from(value: &TimelineUniqueId) -> Self {
888        Self(value.id.clone())
889    }
890}
891
892#[repr(transparent)]
893#[derive(Clone, uniffi::Object)]
894pub struct TimelineItem(pub(crate) matrix_sdk_ui::timeline::TimelineItem);
895
896impl TimelineItem {
897    pub(crate) fn from_arc(arc: Arc<matrix_sdk_ui::timeline::TimelineItem>) -> Arc<Self> {
898        // SAFETY: This is valid because Self is a repr(transparent) wrapper
899        //         around the other Timeline type.
900        unsafe { Arc::from_raw(Arc::into_raw(arc) as _) }
901    }
902}
903
904#[matrix_sdk_ffi_macros::export]
905impl TimelineItem {
906    pub fn as_event(self: Arc<Self>) -> Option<EventTimelineItem> {
907        let event_item = self.0.as_event()?;
908        Some(event_item.clone().into())
909    }
910
911    pub fn as_virtual(self: Arc<Self>) -> Option<VirtualTimelineItem> {
912        use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem;
913        match self.0.as_virtual()? {
914            VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }),
915            VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
916            VItem::TimelineStart => Some(VirtualTimelineItem::TimelineStart),
917        }
918    }
919
920    /// An opaque unique identifier for this timeline item.
921    pub fn unique_id(&self) -> TimelineUniqueId {
922        self.0.unique_id().into()
923    }
924
925    pub fn fmt_debug(&self) -> String {
926        format!("{:#?}", self.0)
927    }
928}
929
930/// This type represents the “send state” of a local event timeline item.
931#[derive(Clone, uniffi::Enum)]
932pub enum EventSendState {
933    /// The local event has not been sent yet.
934    NotSentYet {
935        /// The progress of the sending operation, if the event involves a media
936        /// upload.
937        progress: Option<MediaUploadProgress>,
938    },
939
940    /// The local event has been sent to the server, but unsuccessfully: The
941    /// sending has failed.
942    SendingFailed {
943        /// The error reason, with information for the user.
944        error: QueueWedgeError,
945
946        /// Whether the error is considered recoverable or not.
947        ///
948        /// An error that's recoverable will disable the room's send queue,
949        /// while an unrecoverable error will be parked, until the user
950        /// decides to cancel sending it.
951        is_recoverable: bool,
952    },
953
954    /// The local event has been sent successfully to the server.
955    Sent { event_id: String },
956}
957
958impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
959    fn from(value: &matrix_sdk_ui::timeline::EventSendState) -> Self {
960        use matrix_sdk_ui::timeline::EventSendState::*;
961
962        match value {
963            NotSentYet { progress } => {
964                Self::NotSentYet { progress: progress.clone().map(|p| p.into()) }
965            }
966            SendingFailed { error, is_recoverable } => {
967                let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&**error).into();
968                Self::SendingFailed {
969                    is_recoverable: *is_recoverable,
970                    error: as_queue_wedge_error.into(),
971                }
972            }
973            Sent { event_id } => Self::Sent { event_id: event_id.to_string() },
974        }
975    }
976}
977
978/// Recommended decorations for decrypted messages, representing the message's
979/// authenticity properties.
980#[derive(uniffi::Enum, Clone)]
981pub enum ShieldState {
982    /// A red shield with a tooltip containing the associated message should be
983    /// presented.
984    Red { code: ShieldStateCode, message: String },
985    /// A grey shield with a tooltip containing the associated message should be
986    /// presented.
987    Grey { code: ShieldStateCode, message: String },
988    /// No shield should be presented.
989    None,
990}
991
992impl From<SdkShieldState> for ShieldState {
993    fn from(value: SdkShieldState) -> Self {
994        match value {
995            SdkShieldState::Red { code, message } => {
996                Self::Red { code, message: message.to_owned() }
997            }
998            SdkShieldState::Grey { code, message } => {
999                Self::Grey { code, message: message.to_owned() }
1000            }
1001            SdkShieldState::None => Self::None,
1002        }
1003    }
1004}
1005
1006#[derive(Clone, uniffi::Record)]
1007pub struct EventTimelineItem {
1008    /// Indicates that an event is remote.
1009    is_remote: bool,
1010    event_or_transaction_id: EventOrTransactionId,
1011    sender: String,
1012    sender_profile: ProfileDetails,
1013    is_own: bool,
1014    is_editable: bool,
1015    content: TimelineItemContent,
1016    timestamp: Timestamp,
1017    local_send_state: Option<EventSendState>,
1018    local_created_at: Option<u64>,
1019    read_receipts: HashMap<String, Receipt>,
1020    origin: Option<EventItemOrigin>,
1021    can_be_replied_to: bool,
1022    lazy_provider: Arc<LazyTimelineItemProvider>,
1023}
1024
1025impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
1026    fn from(item: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
1027        let item = Arc::new(item);
1028        let lazy_provider = Arc::new(LazyTimelineItemProvider(item.clone()));
1029        let read_receipts =
1030            item.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect();
1031        Self {
1032            is_remote: !item.is_local_echo(),
1033            event_or_transaction_id: item.identifier().into(),
1034            sender: item.sender().to_string(),
1035            sender_profile: item.sender_profile().clone().into(),
1036            is_own: item.is_own(),
1037            is_editable: item.is_editable(),
1038            content: item.content().clone().into(),
1039            timestamp: item.timestamp().into(),
1040            local_send_state: item.send_state().map(|s| s.into()),
1041            local_created_at: item.local_created_at().map(|t| t.0.into()),
1042            read_receipts,
1043            origin: item.origin(),
1044            can_be_replied_to: item.can_be_replied_to(),
1045            lazy_provider,
1046        }
1047    }
1048}
1049
1050#[derive(Clone, uniffi::Record)]
1051pub struct Receipt {
1052    pub timestamp: Option<Timestamp>,
1053}
1054
1055impl From<ruma::events::receipt::Receipt> for Receipt {
1056    fn from(value: ruma::events::receipt::Receipt) -> Self {
1057        Receipt { timestamp: value.ts.map(|ts| ts.into()) }
1058    }
1059}
1060
1061#[derive(Clone, uniffi::Record)]
1062pub struct EventTimelineItemDebugInfo {
1063    model: String,
1064    original_json: Option<String>,
1065    latest_edit_json: Option<String>,
1066}
1067
1068#[derive(Clone, uniffi::Enum)]
1069pub enum ProfileDetails {
1070    Unavailable,
1071    Pending,
1072    Ready { display_name: Option<String>, display_name_ambiguous: bool, avatar_url: Option<String> },
1073    Error { message: String },
1074}
1075
1076impl From<TimelineDetails<Profile>> for ProfileDetails {
1077    fn from(details: TimelineDetails<Profile>) -> Self {
1078        match details {
1079            TimelineDetails::Unavailable => Self::Unavailable,
1080            TimelineDetails::Pending => Self::Pending,
1081            TimelineDetails::Ready(profile) => Self::Ready {
1082                display_name: profile.display_name,
1083                display_name_ambiguous: profile.display_name_ambiguous,
1084                avatar_url: profile.avatar_url.as_ref().map(ToString::to_string),
1085            },
1086            TimelineDetails::Error(e) => Self::Error { message: e.to_string() },
1087        }
1088    }
1089}
1090
1091#[derive(Clone, uniffi::Record)]
1092pub struct PollData {
1093    question: String,
1094    answers: Vec<String>,
1095    max_selections: u8,
1096    poll_kind: PollKind,
1097}
1098
1099impl PollData {
1100    fn fallback_text(&self) -> String {
1101        self.answers.iter().enumerate().fold(self.question.clone(), |mut acc, (index, answer)| {
1102            write!(&mut acc, "\n{}. {answer}", index + 1).unwrap();
1103            acc
1104        })
1105    }
1106}
1107
1108impl TryFrom<PollData> for UnstablePollStartContentBlock {
1109    type Error = ClientError;
1110
1111    fn try_from(value: PollData) -> Result<Self, Self::Error> {
1112        let poll_answers_vec: Vec<UnstablePollAnswer> = value
1113            .answers
1114            .iter()
1115            .map(|answer| UnstablePollAnswer::new(Uuid::new_v4().to_string(), answer))
1116            .collect();
1117
1118        let poll_answers = UnstablePollAnswers::try_from(poll_answers_vec)
1119            .context("Failed to create poll answers")?;
1120
1121        let mut poll_content_block =
1122            UnstablePollStartContentBlock::new(value.question.clone(), poll_answers);
1123        poll_content_block.kind = value.poll_kind.into();
1124        poll_content_block.max_selections = value.max_selections.into();
1125
1126        Ok(poll_content_block)
1127    }
1128}
1129
1130#[derive(uniffi::Object)]
1131pub struct SendAttachmentJoinHandle {
1132    join_handle: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
1133    abort_handle: AbortHandle,
1134}
1135
1136impl SendAttachmentJoinHandle {
1137    fn new(join_handle: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
1138        let abort_handle = join_handle.abort_handle();
1139        let join_handle = Arc::new(Mutex::new(join_handle));
1140        Arc::new(Self { join_handle, abort_handle })
1141    }
1142}
1143
1144#[matrix_sdk_ffi_macros::export]
1145impl SendAttachmentJoinHandle {
1146    /// Wait until the attachment has been sent.
1147    ///
1148    /// If the sending had been cancelled, will return immediately.
1149    pub async fn join(&self) -> Result<(), RoomError> {
1150        let handle = self.join_handle.clone();
1151        let mut locked_handle = handle.lock().await;
1152        let join_result = (&mut *locked_handle).await;
1153        match join_result {
1154            Ok(res) => res,
1155            Err(err) => {
1156                if err.is_cancelled() {
1157                    return Ok(());
1158                }
1159                error!("task panicked! resuming panic from here.");
1160                #[cfg(not(target_family = "wasm"))]
1161                panic::resume_unwind(err.into_panic());
1162                #[cfg(target_family = "wasm")]
1163                panic!("task panicked! {err}");
1164            }
1165        }
1166    }
1167
1168    /// Cancel the current sending task.
1169    ///
1170    /// A subsequent call to [`Self::join`] will return immediately.
1171    pub fn cancel(&self) {
1172        self.abort_handle.abort();
1173    }
1174}
1175
1176/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
1177#[derive(uniffi::Enum)]
1178pub enum VirtualTimelineItem {
1179    /// A divider between messages of different day or month depending on
1180    /// timeline settings.
1181    DateDivider {
1182        /// A timestamp in milliseconds since Unix Epoch on that day in local
1183        /// time.
1184        ts: Timestamp,
1185    },
1186
1187    /// The user's own read marker.
1188    ReadMarker,
1189
1190    /// The timeline start, that is, the *oldest* event in time for that room.
1191    TimelineStart,
1192}
1193
1194/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
1195#[derive(uniffi::Enum)]
1196pub enum ReceiptType {
1197    Read,
1198    ReadPrivate,
1199    FullyRead,
1200}
1201
1202impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::ReceiptType {
1203    fn from(value: ReceiptType) -> Self {
1204        match value {
1205            ReceiptType::Read => Self::Read,
1206            ReceiptType::ReadPrivate => Self::ReadPrivate,
1207            ReceiptType::FullyRead => Self::FullyRead,
1208        }
1209    }
1210}
1211
1212#[derive(Clone, uniffi::Enum)]
1213pub enum EditedContent {
1214    RoomMessage {
1215        content: Arc<RoomMessageEventContentWithoutRelation>,
1216    },
1217    MediaCaption {
1218        caption: Option<String>,
1219        formatted_caption: Option<FormattedBody>,
1220        mentions: Option<Mentions>,
1221    },
1222    PollStart {
1223        poll_data: PollData,
1224    },
1225}
1226
1227impl TryFrom<EditedContent> for SdkEditedContent {
1228    type Error = ClientError;
1229
1230    fn try_from(value: EditedContent) -> Result<Self, Self::Error> {
1231        match value {
1232            EditedContent::RoomMessage { content } => {
1233                Ok(SdkEditedContent::RoomMessage((*content).clone()))
1234            }
1235            EditedContent::MediaCaption { caption, formatted_caption, mentions } => {
1236                Ok(SdkEditedContent::MediaCaption {
1237                    caption,
1238                    formatted_caption: formatted_caption.map(Into::into),
1239                    mentions: mentions.map(Into::into),
1240                })
1241            }
1242            EditedContent::PollStart { poll_data } => {
1243                let block: UnstablePollStartContentBlock = poll_data.clone().try_into()?;
1244                Ok(SdkEditedContent::PollStart {
1245                    fallback_text: poll_data.fallback_text(),
1246                    new_content: block,
1247                })
1248            }
1249        }
1250    }
1251}
1252
1253/// Create a caption edit.
1254///
1255/// If no `formatted_caption` is provided, then it's assumed the `caption`
1256/// represents valid Markdown that can be used as the formatted caption.
1257#[matrix_sdk_ffi_macros::export]
1258fn create_caption_edit(
1259    caption: Option<String>,
1260    formatted_caption: Option<FormattedBody>,
1261    mentions: Option<Mentions>,
1262) -> EditedContent {
1263    let formatted_caption =
1264        formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
1265    EditedContent::MediaCaption {
1266        caption,
1267        formatted_caption: formatted_caption.as_ref().map(Into::into),
1268        mentions,
1269    }
1270}
1271
1272/// Wrapper to retrieve some timeline item info lazily.
1273#[derive(Clone, uniffi::Object)]
1274pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
1275
1276#[matrix_sdk_ffi_macros::export]
1277impl LazyTimelineItemProvider {
1278    /// Returns the shields for this event timeline item.
1279    fn get_shields(&self, strict: bool) -> Option<ShieldState> {
1280        self.0.get_shield(strict).map(Into::into)
1281    }
1282
1283    /// Returns some debug information for this event timeline item.
1284    fn debug_info(&self) -> EventTimelineItemDebugInfo {
1285        EventTimelineItemDebugInfo {
1286            model: format!("{:#?}", self.0),
1287            original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()),
1288            latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
1289        }
1290    }
1291
1292    /// For local echoes, return the associated send handle; returns `None` for
1293    /// remote echoes.
1294    fn get_send_handle(&self) -> Option<Arc<SendHandle>> {
1295        self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
1296    }
1297
1298    fn contains_only_emojis(&self) -> bool {
1299        self.0.contains_only_emojis()
1300    }
1301}
1302
1303/// Mimic the [`UiLatestEventValue`] type.
1304#[derive(Clone, uniffi::Enum)]
1305pub enum LatestEventValue {
1306    None,
1307    Remote {
1308        timestamp: Timestamp,
1309        sender: String,
1310        is_own: bool,
1311        profile: ProfileDetails,
1312        content: TimelineItemContent,
1313    },
1314    Local {
1315        timestamp: Timestamp,
1316        sender: String,
1317        profile: ProfileDetails,
1318        content: TimelineItemContent,
1319        is_sending: bool,
1320    },
1321}
1322
1323impl From<UiLatestEventValue> for LatestEventValue {
1324    fn from(value: UiLatestEventValue) -> Self {
1325        match value {
1326            UiLatestEventValue::None => Self::None,
1327            UiLatestEventValue::Remote { timestamp, sender, is_own, profile, content } => {
1328                Self::Remote {
1329                    timestamp: timestamp.into(),
1330                    sender: sender.to_string(),
1331                    is_own,
1332                    profile: profile.into(),
1333                    content: content.into(),
1334                }
1335            }
1336            UiLatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
1337                Self::Local {
1338                    timestamp: timestamp.into(),
1339                    sender: sender.to_string(),
1340                    profile: profile.into(),
1341                    content: content.into(),
1342                    is_sending,
1343                }
1344            }
1345        }
1346    }
1347}
1348
1349#[cfg(feature = "unstable-msc4274")]
1350mod galleries {
1351    use std::{panic, sync::Arc};
1352
1353    use matrix_sdk::{
1354        attachment::{
1355            AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
1356        },
1357        utils::formatted_body_from,
1358    };
1359    use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
1360    use matrix_sdk_ui::timeline::GalleryConfig;
1361    use mime::Mime;
1362    use ruma::{assign, events::room::message::TextMessageEventContent, EventId};
1363    use tokio::sync::Mutex;
1364    use tracing::error;
1365
1366    use crate::{
1367        error::RoomError,
1368        ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
1369        runtime::get_runtime_handle,
1370        timeline::{build_thumbnail_info, Timeline, UploadSource},
1371    };
1372
1373    #[derive(uniffi::Record)]
1374    pub struct GalleryUploadParameters {
1375        /// Optional non-formatted caption, for clients that support it.
1376        caption: Option<String>,
1377        /// Optional HTML-formatted caption, for clients that support it.
1378        formatted_caption: Option<FormattedBody>,
1379        /// Optional intentional mentions to be sent with the gallery.
1380        mentions: Option<Mentions>,
1381        /// Optional Event ID to reply to.
1382        in_reply_to: Option<String>,
1383    }
1384
1385    #[derive(uniffi::Enum)]
1386    pub enum GalleryItemInfo {
1387        Audio {
1388            audio_info: AudioInfo,
1389            source: UploadSource,
1390            caption: Option<String>,
1391            formatted_caption: Option<FormattedBody>,
1392        },
1393        File {
1394            file_info: FileInfo,
1395            source: UploadSource,
1396            caption: Option<String>,
1397            formatted_caption: Option<FormattedBody>,
1398        },
1399        Image {
1400            image_info: ImageInfo,
1401            source: UploadSource,
1402            caption: Option<String>,
1403            formatted_caption: Option<FormattedBody>,
1404            thumbnail_source: Option<UploadSource>,
1405        },
1406        Video {
1407            video_info: VideoInfo,
1408            source: UploadSource,
1409            caption: Option<String>,
1410            formatted_caption: Option<FormattedBody>,
1411            thumbnail_source: Option<UploadSource>,
1412        },
1413    }
1414
1415    impl GalleryItemInfo {
1416        fn mimetype(&self) -> &Option<String> {
1417            match self {
1418                GalleryItemInfo::Audio { audio_info, .. } => &audio_info.mimetype,
1419                GalleryItemInfo::File { file_info, .. } => &file_info.mimetype,
1420                GalleryItemInfo::Image { image_info, .. } => &image_info.mimetype,
1421                GalleryItemInfo::Video { video_info, .. } => &video_info.mimetype,
1422            }
1423        }
1424
1425        fn source(&self) -> &UploadSource {
1426            match self {
1427                GalleryItemInfo::File { source, .. } => source,
1428                GalleryItemInfo::Audio { source, .. } => source,
1429                GalleryItemInfo::Image { source, .. } => source,
1430                GalleryItemInfo::Video { source, .. } => source,
1431            }
1432        }
1433
1434        fn caption(&self) -> &Option<String> {
1435            match self {
1436                GalleryItemInfo::Audio { caption, .. } => caption,
1437                GalleryItemInfo::File { caption, .. } => caption,
1438                GalleryItemInfo::Image { caption, .. } => caption,
1439                GalleryItemInfo::Video { caption, .. } => caption,
1440            }
1441        }
1442
1443        fn formatted_caption(&self) -> &Option<FormattedBody> {
1444            match self {
1445                GalleryItemInfo::Audio { formatted_caption, .. } => formatted_caption,
1446                GalleryItemInfo::File { formatted_caption, .. } => formatted_caption,
1447                GalleryItemInfo::Image { formatted_caption, .. } => formatted_caption,
1448                GalleryItemInfo::Video { formatted_caption, .. } => formatted_caption,
1449            }
1450        }
1451
1452        fn attachment_info(&self) -> Result<AttachmentInfo, RoomError> {
1453            match self {
1454                GalleryItemInfo::Audio { audio_info, .. } => Ok(AttachmentInfo::Audio(
1455                    BaseAudioInfo::try_from(audio_info)
1456                        .map_err(|_| RoomError::InvalidAttachmentData)?,
1457                )),
1458                GalleryItemInfo::File { file_info, .. } => Ok(AttachmentInfo::File(
1459                    BaseFileInfo::try_from(file_info)
1460                        .map_err(|_| RoomError::InvalidAttachmentData)?,
1461                )),
1462                GalleryItemInfo::Image { image_info, .. } => Ok(AttachmentInfo::Image(
1463                    BaseImageInfo::try_from(image_info)
1464                        .map_err(|_| RoomError::InvalidAttachmentData)?,
1465                )),
1466                GalleryItemInfo::Video { video_info, .. } => Ok(AttachmentInfo::Video(
1467                    BaseVideoInfo::try_from(video_info)
1468                        .map_err(|_| RoomError::InvalidAttachmentData)?,
1469                )),
1470            }
1471        }
1472
1473        fn thumbnail(&self) -> Result<Option<Thumbnail>, RoomError> {
1474            match self {
1475                GalleryItemInfo::Audio { .. } | GalleryItemInfo::File { .. } => Ok(None),
1476                GalleryItemInfo::Image { image_info, thumbnail_source, .. } => {
1477                    build_thumbnail_info(
1478                        thumbnail_source.as_ref().cloned(),
1479                        image_info.thumbnail_info.clone(),
1480                    )
1481                }
1482                GalleryItemInfo::Video { video_info, thumbnail_source, .. } => {
1483                    build_thumbnail_info(
1484                        thumbnail_source.as_ref().cloned(),
1485                        video_info.thumbnail_info.clone(),
1486                    )
1487                }
1488            }
1489        }
1490    }
1491
1492    impl TryInto<matrix_sdk_ui::timeline::GalleryItemInfo> for GalleryItemInfo {
1493        type Error = RoomError;
1494
1495        fn try_into(
1496            self,
1497        ) -> std::result::Result<matrix_sdk_ui::timeline::GalleryItemInfo, Self::Error> {
1498            let mime_str = self.mimetype().as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
1499            let mime_type =
1500                mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
1501            let caption = self.caption().as_ref().map(|caption| {
1502                let formatted = formatted_body_from(
1503                    Some(caption),
1504                    self.formatted_caption().clone().map(Into::into),
1505                );
1506                assign!(TextMessageEventContent::plain(caption), { formatted })
1507            });
1508            Ok(matrix_sdk_ui::timeline::GalleryItemInfo {
1509                source: self.source().clone().into(),
1510                content_type: mime_type,
1511                attachment_info: self.attachment_info()?,
1512                caption,
1513                thumbnail: self.thumbnail()?,
1514            })
1515        }
1516    }
1517
1518    #[derive(uniffi::Object)]
1519    pub struct SendGalleryJoinHandle {
1520        join_handle: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
1521        abort_handle: AbortHandle,
1522    }
1523
1524    impl SendGalleryJoinHandle {
1525        fn new(join_handle: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
1526            let abort_handle = join_handle.abort_handle();
1527            let join_handle = Arc::new(Mutex::new(join_handle));
1528            Arc::new(Self { join_handle, abort_handle })
1529        }
1530    }
1531
1532    #[matrix_sdk_ffi_macros::export]
1533    impl SendGalleryJoinHandle {
1534        /// Wait until the gallery has been sent.
1535        ///
1536        /// If the sending had been cancelled, will return immediately.
1537        pub async fn join(&self) -> Result<(), RoomError> {
1538            let handle = self.join_handle.clone();
1539            let mut locked_handle = handle.lock().await;
1540            let join_result = (&mut *locked_handle).await;
1541            match join_result {
1542                Ok(res) => res,
1543                Err(err) => {
1544                    if err.is_cancelled() {
1545                        return Ok(());
1546                    }
1547                    error!("task panicked! resuming panic from here.");
1548                    #[cfg(not(target_family = "wasm"))]
1549                    panic::resume_unwind(err.into_panic());
1550                    #[cfg(target_family = "wasm")]
1551                    panic!("task panicked! {err}");
1552                }
1553            }
1554        }
1555
1556        /// Cancel the current sending task.
1557        ///
1558        /// A subsequent call to [`Self::join`] will return immediately.
1559        pub fn cancel(&self) {
1560            self.abort_handle.abort();
1561        }
1562    }
1563
1564    #[matrix_sdk_ffi_macros::export]
1565    impl Timeline {
1566        pub fn send_gallery(
1567            self: Arc<Self>,
1568            params: GalleryUploadParameters,
1569            item_infos: Vec<GalleryItemInfo>,
1570        ) -> Result<Arc<SendGalleryJoinHandle>, RoomError> {
1571            let caption = params.caption.map(|caption| {
1572                let formatted =
1573                    formatted_body_from(Some(&caption), params.formatted_caption.map(Into::into));
1574                assign!(TextMessageEventContent::plain(caption), { formatted })
1575            });
1576
1577            let in_reply_to = params
1578                .in_reply_to
1579                .as_ref()
1580                .map(EventId::parse)
1581                .transpose()
1582                .map_err(|_| RoomError::InvalidRepliedToEventId)?;
1583
1584            let mut gallery_config = GalleryConfig::new()
1585                .caption(caption)
1586                .mentions(params.mentions.map(Into::into))
1587                .in_reply_to(in_reply_to);
1588
1589            for item_info in item_infos {
1590                gallery_config = gallery_config.add_item(item_info.try_into()?);
1591            }
1592
1593            let handle = SendGalleryJoinHandle::new(get_runtime_handle().spawn(async move {
1594                let request = self.inner.send_gallery(gallery_config);
1595                request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
1596                Ok(())
1597            }));
1598
1599            Ok(handle)
1600        }
1601    }
1602}