1use 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: UploadSource,
196 caption: Option<String>,
198 formatted_caption: Option<FormattedBody>,
200 mentions: Option<Mentions>,
202 in_reply_to: Option<String>,
204}
205
206#[derive(Clone, uniffi::Enum)]
208pub enum UploadSource {
209 File {
211 filename: String,
213 },
214 Data {
216 bytes: Vec<u8>,
218 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#[derive(Clone, Copy, uniffi::Record)]
235pub struct MediaUploadProgress {
236 pub index: u64,
240
241 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#[derive(Clone, Copy, uniffi::Record)]
260pub struct AbstractProgress {
261 pub current: u64,
263 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 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 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 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 pub async fn paginate_backwards(&self, num_events: u16) -> Result<bool, ClientError> {
337 Ok(self.inner.paginate_backwards(num_events).await?)
338 }
339
340 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 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 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 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 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 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 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 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 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 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 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 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 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#[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 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 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 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 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#[derive(Clone, uniffi::Enum)]
932pub enum EventSendState {
933 NotSentYet {
935 progress: Option<MediaUploadProgress>,
938 },
939
940 SendingFailed {
943 error: QueueWedgeError,
945
946 is_recoverable: bool,
952 },
953
954 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#[derive(uniffi::Enum, Clone)]
981pub enum ShieldState {
982 Red { code: ShieldStateCode, message: String },
985 Grey { code: ShieldStateCode, message: String },
988 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 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 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 pub fn cancel(&self) {
1172 self.abort_handle.abort();
1173 }
1174}
1175
1176#[derive(uniffi::Enum)]
1178pub enum VirtualTimelineItem {
1179 DateDivider {
1182 ts: Timestamp,
1185 },
1186
1187 ReadMarker,
1189
1190 TimelineStart,
1192}
1193
1194#[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#[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#[derive(Clone, uniffi::Object)]
1274pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
1275
1276#[matrix_sdk_ffi_macros::export]
1277impl LazyTimelineItemProvider {
1278 fn get_shields(&self, strict: bool) -> Option<ShieldState> {
1280 self.0.get_shield(strict).map(Into::into)
1281 }
1282
1283 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 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#[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 caption: Option<String>,
1377 formatted_caption: Option<FormattedBody>,
1379 mentions: Option<Mentions>,
1381 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 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 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}