diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index d75f3f3f370..5cac359d7fe 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -32,6 +32,8 @@ Additions: - Add `Client::observe_account_data_event` and `Client::observe_room_account_data_event` to subscribe to global and room account data changes. ([#4994](https://github.com/matrix-org/matrix-rust-sdk/pull/4994)) +- Add `Timeline::send_gallery` to send MSC4274-style galleries. + ([#5163](https://github.com/matrix-org/matrix-rust-sdk/pull/5163)) Breaking changes: diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index f5f441e9cf4..b9b603f37e8 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -17,8 +17,9 @@ release = true crate-type = ["cdylib", "staticlib"] [features] -default = ["bundled-sqlite", "matrix-sdk-ui/unstable-msc4274"] +default = ["bundled-sqlite", "unstable-msc4274"] bundled-sqlite = ["matrix-sdk/bundled-sqlite"] +unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"] [dependencies] anyhow.workspace = true diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index 5e7ff0a473e..98e636a9804 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -387,6 +387,8 @@ pub enum RoomMessageEventMessageType { Audio, Emote, File, + #[cfg(feature = "unstable-msc4274")] + Gallery, Image, Location, Notice, @@ -403,6 +405,8 @@ impl From for RoomMessageEventMessageType { RumaMessageType::Audio { .. } => Self::Audio, RumaMessageType::Emote { .. } => Self::Emote, RumaMessageType::File { .. } => Self::File, + #[cfg(feature = "unstable-msc4274")] + RumaMessageType::Gallery { .. } => Self::Gallery, RumaMessageType::Image { .. } => Self::Image, RumaMessageType::Location { .. } => Self::Location, RumaMessageType::Notice { .. } => Self::Notice, diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 7592afaf1d5..af0a7531d18 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -345,15 +345,38 @@ impl From for ruma::events::Mentions { #[derive(Clone, uniffi::Enum)] pub enum MessageType { - Emote { content: EmoteMessageContent }, - Image { content: ImageMessageContent }, - Audio { content: AudioMessageContent }, - Video { content: VideoMessageContent }, - File { content: FileMessageContent }, - Notice { content: NoticeMessageContent }, - Text { content: TextMessageContent }, - Location { content: LocationContent }, - Other { msgtype: String, body: String }, + Emote { + content: EmoteMessageContent, + }, + Image { + content: ImageMessageContent, + }, + Audio { + content: AudioMessageContent, + }, + Video { + content: VideoMessageContent, + }, + File { + content: FileMessageContent, + }, + #[cfg(feature = "unstable-msc4274")] + Gallery { + content: GalleryMessageContent, + }, + Notice { + content: NoticeMessageContent, + }, + Text { + content: TextMessageContent, + }, + Location { + content: LocationContent, + }, + Other { + msgtype: String, + body: String, + }, } /// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md @@ -383,44 +406,12 @@ impl TryFrom for RumaMessageType { formatted: content.formatted.map(Into::into), })) } - MessageType::Image { content } => { - let (body, filename) = get_body_and_filename(content.filename, content.caption); - let mut event_content = - RumaImageMessageEventContent::new(body, (*content.source).clone().into()) - .info(content.info.map(Into::into).map(Box::new)); - event_content.formatted = content.formatted_caption.map(Into::into); - event_content.filename = filename; - Self::Image(event_content) - } - MessageType::Audio { content } => { - let (body, filename) = get_body_and_filename(content.filename, content.caption); - let mut event_content = - RumaAudioMessageEventContent::new(body, (*content.source).clone().into()) - .info(content.info.map(Into::into).map(Box::new)); - event_content.formatted = content.formatted_caption.map(Into::into); - event_content.filename = filename; - event_content.audio = content.audio.map(Into::into); - event_content.voice = content.voice.map(Into::into); - Self::Audio(event_content) - } - MessageType::Video { content } => { - let (body, filename) = get_body_and_filename(content.filename, content.caption); - let mut event_content = - RumaVideoMessageEventContent::new(body, (*content.source).clone().into()) - .info(content.info.map(Into::into).map(Box::new)); - event_content.formatted = content.formatted_caption.map(Into::into); - event_content.filename = filename; - Self::Video(event_content) - } - MessageType::File { content } => { - let (body, filename) = get_body_and_filename(content.filename, content.caption); - let mut event_content = - RumaFileMessageEventContent::new(body, (*content.source).clone().into()) - .info(content.info.map(Into::into).map(Box::new)); - event_content.formatted = content.formatted_caption.map(Into::into); - event_content.filename = filename; - Self::File(event_content) - } + MessageType::Image { content } => Self::Image(content.into()), + MessageType::Audio { content } => Self::Audio(content.into()), + MessageType::Video { content } => Self::Video(content.into()), + MessageType::File { content } => Self::File(content.into()), + #[cfg(feature = "unstable-msc4274")] + MessageType::Gallery { content } => Self::Gallery(content.try_into()?), MessageType::Notice { content } => { Self::Notice(assign!(RumaNoticeMessageEventContent::plain(content.body), { formatted: content.formatted.map(Into::into), @@ -452,45 +443,12 @@ impl TryFrom for MessageType { formatted: c.formatted.as_ref().map(Into::into), }, }, - RumaMessageType::Image(c) => MessageType::Image { - content: ImageMessageContent { - filename: c.filename().to_owned(), - caption: c.caption().map(ToString::to_string), - formatted_caption: c.formatted_caption().map(Into::into), - source: Arc::new(c.source.try_into()?), - info: c.info.as_deref().map(TryInto::try_into).transpose()?, - }, - }, - - RumaMessageType::Audio(c) => MessageType::Audio { - content: AudioMessageContent { - filename: c.filename().to_owned(), - caption: c.caption().map(ToString::to_string), - formatted_caption: c.formatted_caption().map(Into::into), - source: Arc::new(c.source.try_into()?), - info: c.info.as_deref().map(Into::into), - audio: c.audio.map(Into::into), - voice: c.voice.map(Into::into), - }, - }, - RumaMessageType::Video(c) => MessageType::Video { - content: VideoMessageContent { - filename: c.filename().to_owned(), - caption: c.caption().map(ToString::to_string), - formatted_caption: c.formatted_caption().map(Into::into), - source: Arc::new(c.source.try_into()?), - info: c.info.as_deref().map(TryInto::try_into).transpose()?, - }, - }, - RumaMessageType::File(c) => MessageType::File { - content: FileMessageContent { - filename: c.filename().to_owned(), - caption: c.caption().map(ToString::to_string), - formatted_caption: c.formatted_caption().map(Into::into), - source: Arc::new(c.source.try_into()?), - info: c.info.as_deref().map(TryInto::try_into).transpose()?, - }, - }, + RumaMessageType::Image(c) => MessageType::Image { content: c.try_into()? }, + RumaMessageType::Audio(c) => MessageType::Audio { content: c.try_into()? }, + RumaMessageType::Video(c) => MessageType::Video { content: c.try_into()? }, + RumaMessageType::File(c) => MessageType::File { content: c.try_into()? }, + #[cfg(feature = "unstable-msc4274")] + RumaMessageType::Gallery(c) => MessageType::Gallery { content: c.try_into()? }, RumaMessageType::Notice(c) => MessageType::Notice { content: NoticeMessageContent { body: c.body.clone(), @@ -568,6 +526,31 @@ pub struct ImageMessageContent { pub info: Option, } +impl From for RumaImageMessageEventContent { + fn from(value: ImageMessageContent) -> Self { + let (body, filename) = get_body_and_filename(value.filename, value.caption); + let mut event_content = Self::new(body, (*value.source).clone().into()) + .info(value.info.map(Into::into).map(Box::new)); + event_content.formatted = value.formatted_caption.map(Into::into); + event_content.filename = filename; + event_content + } +} + +impl TryFrom for ImageMessageContent { + type Error = ClientError; + + fn try_from(value: RumaImageMessageEventContent) -> Result { + Ok(Self { + filename: value.filename().to_owned(), + caption: value.caption().map(ToString::to_string), + formatted_caption: value.formatted_caption().map(Into::into), + source: Arc::new(value.source.try_into()?), + info: value.info.as_deref().map(TryInto::try_into).transpose()?, + }) + } +} + #[derive(Clone, uniffi::Record)] pub struct AudioMessageContent { /// The computed filename, for use in a client. @@ -580,6 +563,35 @@ pub struct AudioMessageContent { pub voice: Option, } +impl From for RumaAudioMessageEventContent { + fn from(value: AudioMessageContent) -> Self { + let (body, filename) = get_body_and_filename(value.filename, value.caption); + let mut event_content = Self::new(body, (*value.source).clone().into()) + .info(value.info.map(Into::into).map(Box::new)); + event_content.formatted = value.formatted_caption.map(Into::into); + event_content.filename = filename; + event_content.audio = value.audio.map(Into::into); + event_content.voice = value.voice.map(Into::into); + event_content + } +} + +impl TryFrom for AudioMessageContent { + type Error = ClientError; + + fn try_from(value: RumaAudioMessageEventContent) -> Result { + Ok(Self { + filename: value.filename().to_owned(), + caption: value.caption().map(ToString::to_string), + formatted_caption: value.formatted_caption().map(Into::into), + source: Arc::new(value.source.try_into()?), + info: value.info.as_deref().map(Into::into), + audio: value.audio.map(Into::into), + voice: value.voice.map(Into::into), + }) + } +} + #[derive(Clone, uniffi::Record)] pub struct VideoMessageContent { /// The computed filename, for use in a client. @@ -590,6 +602,31 @@ pub struct VideoMessageContent { pub info: Option, } +impl From for RumaVideoMessageEventContent { + fn from(value: VideoMessageContent) -> Self { + let (body, filename) = get_body_and_filename(value.filename, value.caption); + let mut event_content = Self::new(body, (*value.source).clone().into()) + .info(value.info.map(Into::into).map(Box::new)); + event_content.formatted = value.formatted_caption.map(Into::into); + event_content.filename = filename; + event_content + } +} + +impl TryFrom for VideoMessageContent { + type Error = ClientError; + + fn try_from(value: RumaVideoMessageEventContent) -> Result { + Ok(Self { + filename: value.filename().to_owned(), + caption: value.caption().map(ToString::to_string), + formatted_caption: value.formatted_caption().map(Into::into), + source: Arc::new(value.source.try_into()?), + info: value.info.as_deref().map(TryInto::try_into).transpose()?, + }) + } +} + #[derive(Clone, uniffi::Record)] pub struct FileMessageContent { /// The computed filename, for use in a client. @@ -600,6 +637,31 @@ pub struct FileMessageContent { pub info: Option, } +impl From for RumaFileMessageEventContent { + fn from(value: FileMessageContent) -> Self { + let (body, filename) = get_body_and_filename(value.filename, value.caption); + let mut event_content = Self::new(body, (*value.source).clone().into()) + .info(value.info.map(Into::into).map(Box::new)); + event_content.formatted = value.formatted_caption.map(Into::into); + event_content.filename = filename; + event_content + } +} + +impl TryFrom for FileMessageContent { + type Error = ClientError; + + fn try_from(value: RumaFileMessageEventContent) -> Result { + Ok(Self { + filename: value.filename().to_owned(), + caption: value.caption().map(ToString::to_string), + formatted_caption: value.formatted_caption().map(Into::into), + source: Arc::new(value.source.try_into()?), + info: value.info.as_deref().map(TryInto::try_into).transpose()?, + }) + } +} + #[derive(Clone, uniffi::Record)] pub struct ImageInfo { pub height: Option, @@ -1694,3 +1756,102 @@ impl From> for RoomAc Self::UnstableMarkedUnread { unread: value.content.unread } } } + +#[cfg(feature = "unstable-msc4274")] +pub use galleries::*; + +#[cfg(feature = "unstable-msc4274")] +mod galleries { + use ruma::{ + events::room::message::{ + FormattedBody as RumaFormattedBody, GalleryItemType as RumaGalleryItemType, + GalleryMessageEventContent as RumaGalleryMessageEventContent, + }, + serde::JsonObject, + }; + + use crate::{ + error::ClientError, + ruma::{ + AudioMessageContent, FileMessageContent, FormattedBody, ImageMessageContent, + VideoMessageContent, + }, + }; + + #[derive(Clone, uniffi::Record)] + pub struct GalleryMessageContent { + pub body: String, + pub formatted: Option, + pub itemtypes: Vec, + } + + impl TryFrom for RumaGalleryMessageEventContent { + type Error = ClientError; + + fn try_from(value: GalleryMessageContent) -> Result { + Ok(Self::new( + value.body, + value.formatted.map(Into::into), + value.itemtypes.into_iter().map(TryInto::try_into).collect::>()?, + )) + } + } + + impl TryFrom for GalleryMessageContent { + type Error = ClientError; + + fn try_from(value: RumaGalleryMessageEventContent) -> Result { + Ok(Self { + body: value.body, + formatted: value.formatted.as_ref().map(Into::into), + itemtypes: value + .itemtypes + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + }) + } + } + + #[derive(Clone, uniffi::Enum)] + pub enum GalleryItemType { + Image { content: ImageMessageContent }, + Audio { content: AudioMessageContent }, + Video { content: VideoMessageContent }, + File { content: FileMessageContent }, + Other { itemtype: String, body: String }, + } + + impl TryFrom for RumaGalleryItemType { + type Error = ClientError; + + fn try_from(value: GalleryItemType) -> Result { + Ok(match value { + GalleryItemType::Image { content } => Self::Image(content.into()), + GalleryItemType::Audio { content } => Self::Audio(content.into()), + GalleryItemType::Video { content } => Self::Video(content.into()), + GalleryItemType::File { content } => Self::File(content.into()), + GalleryItemType::Other { itemtype, body } => { + Self::new(&itemtype, body, JsonObject::default())? + } + }) + } + } + + impl TryFrom for GalleryItemType { + type Error = ClientError; + + fn try_from(value: RumaGalleryItemType) -> Result { + Ok(match value { + RumaGalleryItemType::Image(c) => GalleryItemType::Image { content: c.try_into()? }, + RumaGalleryItemType::Audio(c) => GalleryItemType::Audio { content: c.try_into()? }, + RumaGalleryItemType::Video(c) => GalleryItemType::Video { content: c.try_into()? }, + RumaGalleryItemType::File(c) => GalleryItemType::File { content: c.try_into()? }, + _ => GalleryItemType::Other { + itemtype: value.itemtype().to_owned(), + body: value.body().to_owned(), + }, + }) + } + } +} diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 4d18d5271d5..40cb90043c1 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1200,15 +1200,15 @@ impl TryFrom for UnstablePollStartContentBlock { #[derive(uniffi::Object)] pub struct SendAttachmentJoinHandle { - join_hdl: Arc>>>, - abort_hdl: AbortHandle, + join_handle: Arc>>>, + abort_handle: AbortHandle, } impl SendAttachmentJoinHandle { - fn new(join_hdl: JoinHandle>) -> Arc { - let abort_hdl = join_hdl.abort_handle(); - let join_hdl = Arc::new(Mutex::new(join_hdl)); - Arc::new(Self { join_hdl, abort_hdl }) + fn new(join_handle: JoinHandle>) -> Arc { + let abort_handle = join_handle.abort_handle(); + let join_handle = Arc::new(Mutex::new(join_handle)); + Arc::new(Self { join_handle, abort_handle }) } } @@ -1218,7 +1218,7 @@ impl SendAttachmentJoinHandle { /// /// If the sending had been cancelled, will return immediately. pub async fn join(&self) -> Result<(), RoomError> { - let handle = self.join_hdl.clone(); + let handle = self.join_handle.clone(); let mut locked_handle = handle.lock().await; let join_result = (&mut *locked_handle).await; match join_result { @@ -1237,7 +1237,7 @@ impl SendAttachmentJoinHandle { /// /// A subsequent call to [`Self::join`] will return immediately. pub fn cancel(&self) { - self.abort_hdl.abort(); + self.abort_handle.abort(); } } @@ -1367,3 +1367,237 @@ impl LazyTimelineItemProvider { self.0.contains_only_emojis() } } + +#[cfg(feature = "unstable-msc4274")] +mod galleries { + use std::{panic, sync::Arc}; + + use async_compat::get_runtime_handle; + use matrix_sdk::{ + attachment::{ + AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail, + }, + utils::formatted_body_from, + }; + use matrix_sdk_ui::timeline::GalleryConfig; + use mime::Mime; + use tokio::{ + sync::Mutex, + task::{AbortHandle, JoinHandle}, + }; + use tracing::error; + + use crate::{ + error::RoomError, + ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo}, + timeline::{build_thumbnail_info, Timeline}, + }; + + #[derive(uniffi::Record)] + pub struct GalleryUploadParameters { + /// Optional non-formatted caption, for clients that support it. + caption: Option, + /// Optional HTML-formatted caption, for clients that support it. + formatted_caption: Option, + /// Optional intentional mentions to be sent with the gallery. + mentions: Option, + } + + #[derive(uniffi::Enum)] + pub enum GalleryItemInfo { + Audio { + audio_info: AudioInfo, + filename: String, + caption: Option, + formatted_caption: Option, + }, + File { + file_info: FileInfo, + filename: String, + caption: Option, + formatted_caption: Option, + }, + Image { + image_info: ImageInfo, + filename: String, + caption: Option, + formatted_caption: Option, + thumbnail_path: Option, + }, + Video { + video_info: VideoInfo, + filename: String, + caption: Option, + formatted_caption: Option, + thumbnail_path: Option, + }, + } + + impl GalleryItemInfo { + fn mimetype(&self) -> &Option { + match self { + GalleryItemInfo::Audio { audio_info, .. } => &audio_info.mimetype, + GalleryItemInfo::File { file_info, .. } => &file_info.mimetype, + GalleryItemInfo::Image { image_info, .. } => &image_info.mimetype, + GalleryItemInfo::Video { video_info, .. } => &video_info.mimetype, + } + } + + fn filename(&self) -> &String { + match self { + GalleryItemInfo::Audio { filename, .. } => filename, + GalleryItemInfo::File { filename, .. } => filename, + GalleryItemInfo::Image { filename, .. } => filename, + GalleryItemInfo::Video { filename, .. } => filename, + } + } + + fn caption(&self) -> &Option { + match self { + GalleryItemInfo::Audio { caption, .. } => caption, + GalleryItemInfo::File { caption, .. } => caption, + GalleryItemInfo::Image { caption, .. } => caption, + GalleryItemInfo::Video { caption, .. } => caption, + } + } + + fn formatted_caption(&self) -> &Option { + match self { + GalleryItemInfo::Audio { formatted_caption, .. } => formatted_caption, + GalleryItemInfo::File { formatted_caption, .. } => formatted_caption, + GalleryItemInfo::Image { formatted_caption, .. } => formatted_caption, + GalleryItemInfo::Video { formatted_caption, .. } => formatted_caption, + } + } + + fn attachment_info(&self) -> Result { + match self { + GalleryItemInfo::Audio { audio_info, .. } => Ok(AttachmentInfo::Audio( + BaseAudioInfo::try_from(audio_info) + .map_err(|_| RoomError::InvalidAttachmentData)?, + )), + GalleryItemInfo::File { file_info, .. } => Ok(AttachmentInfo::File( + BaseFileInfo::try_from(file_info) + .map_err(|_| RoomError::InvalidAttachmentData)?, + )), + GalleryItemInfo::Image { image_info, .. } => Ok(AttachmentInfo::Image( + BaseImageInfo::try_from(image_info) + .map_err(|_| RoomError::InvalidAttachmentData)?, + )), + GalleryItemInfo::Video { video_info, .. } => Ok(AttachmentInfo::Video( + BaseVideoInfo::try_from(video_info) + .map_err(|_| RoomError::InvalidAttachmentData)?, + )), + } + } + + fn thumbnail(&self) -> Result, RoomError> { + match self { + GalleryItemInfo::Audio { .. } | GalleryItemInfo::File { .. } => Ok(None), + GalleryItemInfo::Image { image_info, thumbnail_path, .. } => { + build_thumbnail_info(thumbnail_path.clone(), image_info.thumbnail_info.clone()) + } + GalleryItemInfo::Video { video_info, thumbnail_path, .. } => { + build_thumbnail_info(thumbnail_path.clone(), video_info.thumbnail_info.clone()) + } + } + } + } + + impl TryInto for GalleryItemInfo { + type Error = RoomError; + + fn try_into( + self, + ) -> std::result::Result { + let mime_str = self.mimetype().as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?; + let mime_type = + mime_str.parse::().map_err(|_| RoomError::InvalidAttachmentMimeType)?; + Ok(matrix_sdk_ui::timeline::GalleryItemInfo { + source: self.filename().into(), + content_type: mime_type, + attachment_info: self.attachment_info()?, + caption: self.caption().clone(), + formatted_caption: self + .formatted_caption() + .clone() + .map(ruma::events::room::message::FormattedBody::from), + thumbnail: self.thumbnail()?, + }) + } + } + + #[derive(uniffi::Object)] + pub struct SendGalleryJoinHandle { + join_handle: Arc>>>, + abort_handle: AbortHandle, + } + + impl SendGalleryJoinHandle { + fn new(join_handle: JoinHandle>) -> Arc { + let abort_handle = join_handle.abort_handle(); + let join_handle = Arc::new(Mutex::new(join_handle)); + Arc::new(Self { join_handle, abort_handle }) + } + } + + #[matrix_sdk_ffi_macros::export] + impl SendGalleryJoinHandle { + /// Wait until the gallery has been sent. + /// + /// If the sending had been cancelled, will return immediately. + pub async fn join(&self) -> Result<(), RoomError> { + let handle = self.join_handle.clone(); + let mut locked_handle = handle.lock().await; + let join_result = (&mut *locked_handle).await; + match join_result { + Ok(res) => res, + Err(err) => { + if err.is_cancelled() { + return Ok(()); + } + error!("task panicked! resuming panic from here."); + panic::resume_unwind(err.into_panic()); + } + } + } + + /// Cancel the current sending task. + /// + /// A subsequent call to [`Self::join`] will return immediately. + pub fn cancel(&self) { + self.abort_handle.abort(); + } + } + + #[matrix_sdk_ffi_macros::export] + impl Timeline { + pub fn send_gallery( + self: Arc, + params: GalleryUploadParameters, + item_infos: Vec, + ) -> Result, RoomError> { + let formatted_caption = formatted_body_from( + params.caption.as_deref(), + params.formatted_caption.map(Into::into), + ); + + let mut gallery_config = GalleryConfig::new() + .caption(params.caption) + .formatted_caption(formatted_caption) + .mentions(params.mentions.map(Into::into)); + + for item_info in item_infos { + gallery_config = gallery_config.add_item(item_info.try_into()?); + } + + let handle = SendGalleryJoinHandle::new(get_runtime_handle().spawn(async move { + let request = self.inner.send_gallery(gallery_config); + request.await.map_err(|_| RoomError::FailedSendingAttachment)?; + Ok(()) + })); + + Ok(handle) + } + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/futures.rs b/crates/matrix-sdk-ui/src/timeline/futures.rs index bb9af758747..bb1bc37f71f 100644 --- a/crates/matrix-sdk-ui/src/timeline/futures.rs +++ b/crates/matrix-sdk-ui/src/timeline/futures.rs @@ -100,11 +100,11 @@ pub use galleries::*; mod galleries { use std::future::IntoFuture; - use matrix_sdk::attachment::GalleryConfig; use matrix_sdk_base::boxed_into_future; use tracing::{Instrument as _, Span}; use super::{Error, Timeline}; + use crate::timeline::GalleryConfig; pub struct SendGallery<'a> { timeline: &'a Timeline, @@ -127,7 +127,7 @@ mod galleries { let fut = async move { let send_queue = timeline.room().send_queue(); - let fut = send_queue.send_gallery(gallery); + let fut = send_queue.send_gallery(gallery.try_into()?); fut.await.map_err(|_| Error::FailedSendingAttachment)?; Ok(()) diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index cc5cd7fe955..c9ca4ba79ee 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -26,7 +26,7 @@ use futures::SendGallery; use futures_core::Stream; use imbl::Vector; #[cfg(feature = "unstable-msc4274")] -use matrix_sdk::attachment::GalleryConfig; +use matrix_sdk::attachment::{AttachmentInfo, Thumbnail}; use matrix_sdk::{ attachment::AttachmentConfig, deserialized_responses::TimelineEvent, @@ -52,6 +52,11 @@ use ruma::{ }, EventId, OwnedEventId, RoomVersionId, UserId, }; +#[cfg(feature = "unstable-msc4274")] +use ruma::{ + events::{room::message::FormattedBody, Mentions}, + OwnedTransactionId, +}; use subscriber::TimelineWithDropHandle; use thiserror::Error; use tracing::{instrument, trace, warn}; @@ -830,3 +835,162 @@ where Self::File(value.into()) } } + +/// Configuration for sending a gallery. +/// +/// This duplicates [`matrix_sdk::attachment::GalleryConfig`] but uses an +/// `AttachmentSource` so that we can delay loading the actual data until we're +/// inside the SendGallery future. This allows [`Timeline::send_gallery`] to +/// return early without blocking the caller. +#[cfg(feature = "unstable-msc4274")] +#[derive(Debug, Default)] +pub struct GalleryConfig { + pub(crate) txn_id: Option, + pub(crate) items: Vec, + pub(crate) caption: Option, + pub(crate) formatted_caption: Option, + pub(crate) mentions: Option, + pub(crate) reply: Option, +} + +#[cfg(feature = "unstable-msc4274")] +impl GalleryConfig { + /// Create a new empty `GalleryConfig`. + pub fn new() -> Self { + Self::default() + } + + /// Set the transaction ID to send. + /// + /// # Arguments + /// + /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` held + /// in its unsigned field as `transaction_id`. If not given, one is + /// created for the message. + #[must_use] + pub fn txn_id(mut self, txn_id: OwnedTransactionId) -> Self { + self.txn_id = Some(txn_id); + self + } + + /// Adds a media item to the gallery. + /// + /// # Arguments + /// + /// * `item` - Information about the item to be added. + #[must_use] + pub fn add_item(mut self, item: GalleryItemInfo) -> Self { + self.items.push(item); + self + } + + /// Set the optional caption. + /// + /// # Arguments + /// + /// * `caption` - The optional caption. + pub fn caption(mut self, caption: Option) -> Self { + self.caption = caption; + self + } + + /// Set the optional formatted caption. + /// + /// # Arguments + /// + /// * `formatted_caption` - The optional formatted caption. + pub fn formatted_caption(mut self, formatted_caption: Option) -> Self { + self.formatted_caption = formatted_caption; + self + } + + /// Set the mentions of the message. + /// + /// # Arguments + /// + /// * `mentions` - The mentions of the message. + pub fn mentions(mut self, mentions: Option) -> Self { + self.mentions = mentions; + self + } + + /// Set the reply information of the message. + /// + /// # Arguments + /// + /// * `reply` - The reply information of the message. + pub fn reply(mut self, reply: Option) -> Self { + self.reply = reply; + self + } + + /// Returns the number of media items in the gallery. + pub fn len(&self) -> usize { + self.items.len() + } + + /// Checks whether the gallery contains any media items or not. + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } +} + +#[cfg(feature = "unstable-msc4274")] +impl TryFrom for matrix_sdk::attachment::GalleryConfig { + type Error = Error; + + fn try_from(value: GalleryConfig) -> Result { + let mut config = matrix_sdk::attachment::GalleryConfig::new(); + + if let Some(txn_id) = value.txn_id { + config = config.txn_id(txn_id); + } + + for item in value.items { + config = config.add_item(item.try_into()?); + } + + config = config.caption(value.caption); + config = config.formatted_caption(value.formatted_caption); + config = config.mentions(value.mentions); + config = config.reply(value.reply); + + Ok(config) + } +} + +#[cfg(feature = "unstable-msc4274")] +#[derive(Debug)] +/// Metadata for a gallery item +pub struct GalleryItemInfo { + /// The attachment source. + pub source: AttachmentSource, + /// The mime type. + pub content_type: Mime, + /// The attachment info. + pub attachment_info: AttachmentInfo, + /// The caption. + pub caption: Option, + /// The formatted caption. + pub formatted_caption: Option, + /// The thumbnail. + pub thumbnail: Option, +} + +#[cfg(feature = "unstable-msc4274")] +impl TryFrom for matrix_sdk::attachment::GalleryItemInfo { + type Error = Error; + + fn try_from(value: GalleryItemInfo) -> Result { + let (data, filename) = value.source.try_into_bytes_and_filename()?; + Ok(matrix_sdk::attachment::GalleryItemInfo { + filename, + content_type: value.content_type, + data, + attachment_info: value.attachment_info, + caption: value.caption, + formatted_caption: value.formatted_caption, + thumbnail: value.thumbnail, + }) + } +} diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs index 0be1d9c91e4..25d647e63ce 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs @@ -19,7 +19,7 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; #[cfg(feature = "unstable-msc4274")] -use matrix_sdk::attachment::{AttachmentInfo, BaseFileInfo, GalleryConfig, GalleryItemInfo}; +use matrix_sdk::attachment::{AttachmentInfo, BaseFileInfo}; use matrix_sdk::{ assert_let_timeout, attachment::AttachmentConfig, @@ -29,6 +29,8 @@ use matrix_sdk::{ use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE}; use matrix_sdk_ui::timeline::{AttachmentSource, EventSendState, RoomExt}; #[cfg(feature = "unstable-msc4274")] +use matrix_sdk_ui::timeline::{GalleryConfig, GalleryItemInfo}; +#[cfg(feature = "unstable-msc4274")] use ruma::events::room::message::GalleryItemType; #[cfg(feature = "unstable-msc4274")] use ruma::owned_mxc_uri; @@ -328,9 +330,8 @@ async fn test_send_gallery_from_bytes() { // Queue sending of a gallery. let gallery = GalleryConfig::new().caption(Some("caption".to_owned())).add_item(GalleryItemInfo { - filename: filename.to_owned(), + source: AttachmentSource::Data { bytes: data, filename: filename.to_owned() }, content_type: mime::TEXT_PLAIN, - data, attachment_info: AttachmentInfo::File(BaseFileInfo { size: None }), caption: Some("item caption".to_owned()), formatted_caption: None,