Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions bindings/matrix-sdk-ffi/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
#[derive(Debug, PartialEq, Clone, uniffi::Enum)]
pub enum RoomMessageEventMessageType {
Audio,
Custom,
Emote,
File,
#[cfg(feature = "unstable-msc4274")]
Expand All @@ -691,6 +692,7 @@ impl From<RumaMessageType> for RoomMessageEventMessageType {
fn from(val: ruma::events::room::message::MessageType) -> Self {
match val {
RumaMessageType::Audio { .. } => Self::Audio,
RumaMessageType::_Custom(_) => Self::Custom,
RumaMessageType::Emote { .. } => Self::Emote,
RumaMessageType::File { .. } => Self::File,
#[cfg(feature = "unstable-msc4274")]
Expand Down Expand Up @@ -741,3 +743,29 @@ impl TryFrom<EventOrTransactionId> for TimelineEventItemId {
}
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::*;

#[test]
fn room_message_event_message_type_marks_custom_messages_as_custom() {
let custom_message = RumaMessageType::new(
"org.example.letter",
"Dear Alice".to_owned(),
serde_json::from_value(json!({
"letter_id": "lt-123",
"version": 1,
}))
.unwrap(),
)
.unwrap();

assert_eq!(
RoomMessageEventMessageType::from(custom_message),
RoomMessageEventMessageType::Custom
);
}
}
195 changes: 194 additions & 1 deletion bindings/matrix-sdk-ffi/src/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use matrix_sdk::{
DraftAttachment as SdkDraftAttachment, DraftAttachmentContent, DraftThumbnail, EncryptionState,
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
SuccessorRoom as SdkSuccessorRoom,
attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail as SdkThumbnail},
encryption::LocalTrust,
room::{
Room as SdkRoom, RoomMemberRole, edit::EditedContent, power_levels::RoomPowerLevelChanges,
Expand Down Expand Up @@ -62,7 +63,10 @@ use crate::{
live_locations_observer::LiveLocationsObserver,
room_member::{RoomMember, RoomMemberWithSenderInfo},
room_preview::RoomPreview,
ruma::{AudioInfo, FileInfo, ImageInfo, MediaSource, ThumbnailInfo, VideoInfo},
ruma::{
AudioInfo, AudioMessageContent, FileInfo, FileMessageContent, ImageInfo,
ImageMessageContent, MediaSource, ThumbnailInfo, VideoInfo, VideoMessageContent,
},
runtime::get_runtime_handle,
timeline::{
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
Expand Down Expand Up @@ -106,6 +110,32 @@ impl Room {
pub(crate) fn new(inner: SdkRoom, utd_hook_manager: Option<Arc<UtdHookManager>>) -> Self {
Room { inner, utd_hook_manager }
}

async fn upload_attachment_source(
&self,
source: UploadSource,
mime_type: Option<String>,
thumbnail: Option<SdkThumbnail>,
) -> Result<(String, Arc<MediaSource>, Option<Arc<MediaSource>>), ClientError> {
let mime_str = mime_type
.as_ref()
.ok_or(RoomError::InvalidAttachmentMimeType)
.map_err(ClientError::from)?;
let mime_type = mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;

let (data, filename) = read_upload_source(source)?;
let uploaded = self.inner.upload_attachment(&mime_type, data, thumbnail).await?;

let source = Arc::new(uploaded.source.try_into()?);
let thumbnail_source = uploaded
.thumbnail
.as_ref()
.map(|(source, _)| MediaSource::try_from(source))
.transpose()?
.map(Arc::new);

Ok((filename, source, thumbnail_source))
}
}

#[matrix_sdk_ffi_macros::export]
Expand Down Expand Up @@ -438,6 +468,96 @@ impl Room {
Ok(())
}

/// Upload image media for later use in a custom event without sending a media event.
pub async fn upload_image(
&self,
source: UploadSource,
thumbnail_source: Option<UploadSource>,
mut image_info: ImageInfo,
) -> Result<ImageMessageContent, ClientError> {
BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?;

let thumbnail = build_upload_thumbnail(image_info.thumbnail_info.clone(), thumbnail_source)?;
let (filename, source, uploaded_thumbnail_source) =
self.upload_attachment_source(source, image_info.mimetype.clone(), thumbnail).await?;

image_info.thumbnail_source = uploaded_thumbnail_source;

Ok(ImageMessageContent {
filename,
caption: None,
formatted_caption: None,
source,
info: Some(image_info),
})
}

/// Upload video media for later use in a custom event without sending a media event.
pub async fn upload_video(
&self,
source: UploadSource,
thumbnail_source: Option<UploadSource>,
mut video_info: VideoInfo,
) -> Result<VideoMessageContent, ClientError> {
BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?;

let thumbnail = build_upload_thumbnail(video_info.thumbnail_info.clone(), thumbnail_source)?;
let (filename, source, uploaded_thumbnail_source) =
self.upload_attachment_source(source, video_info.mimetype.clone(), thumbnail).await?;

video_info.thumbnail_source = uploaded_thumbnail_source;

Ok(VideoMessageContent {
filename,
caption: None,
formatted_caption: None,
source,
info: Some(video_info),
})
}

/// Upload audio media for later use in a custom event without sending a media event.
pub async fn upload_audio(
&self,
source: UploadSource,
audio_info: AudioInfo,
) -> Result<AudioMessageContent, ClientError> {
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?;

let (filename, source, _) =
self.upload_attachment_source(source, audio_info.mimetype.clone(), None).await?;

Ok(AudioMessageContent {
filename,
caption: None,
formatted_caption: None,
source,
info: Some(audio_info),
audio: None,
voice: None,
})
}

/// Upload file media for later use in a custom event without sending a media event.
pub async fn upload_file(
&self,
source: UploadSource,
file_info: FileInfo,
) -> Result<FileMessageContent, ClientError> {
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;

let (filename, source, _) =
self.upload_attachment_source(source, file_info.mimetype.clone(), None).await?;

Ok(FileMessageContent {
filename,
caption: None,
formatted_caption: None,
source,
info: Some(file_info),
})
}

/// Send a raw state event to the room.
///
/// # Arguments
Expand Down Expand Up @@ -1633,6 +1753,79 @@ fn read_upload_source(source: UploadSource) -> Result<(Vec<u8>, String), ClientE
}
}

fn build_upload_thumbnail(
thumbnail_info: Option<ThumbnailInfo>,
thumbnail_source: Option<UploadSource>,
) -> Result<Option<SdkThumbnail>, ClientError> {
match (thumbnail_info, thumbnail_source) {
(Some(info), Some(source)) => {
let mime_type = info
.mimetype
.as_ref()
.ok_or(RoomError::InvalidThumbnailData)
.map_err(ClientError::from)?
.parse::<Mime>()
.map_err(|_| RoomError::InvalidThumbnailData)?;
let height = u64_to_uint(info.height.ok_or(RoomError::InvalidThumbnailData)?);
let width = u64_to_uint(info.width.ok_or(RoomError::InvalidThumbnailData)?);
let size = u64_to_uint(info.size.ok_or(RoomError::InvalidThumbnailData)?);
let (data, _) = read_upload_source(source)?;

Ok(Some(SdkThumbnail { data, content_type: mime_type, height, width, size }))
}
(None, None) => Ok(None),
_ => Err(ClientError::from(RoomError::InvalidThumbnailData)),
}
}

#[cfg(test)]
mod tests {
use super::*;

fn thumbnail_info() -> ThumbnailInfo {
ThumbnailInfo {
height: Some(60),
width: Some(80),
mimetype: Some("image/jpeg".to_owned()),
size: Some(128),
}
}

fn thumbnail_source() -> UploadSource {
UploadSource::Data {
bytes: vec![1, 2, 3],
filename: "thumb.jpg".to_owned(),
}
}

#[test]
fn build_upload_thumbnail_rejects_missing_thumbnail_source() {
let result = build_upload_thumbnail(Some(thumbnail_info()), None);

assert!(matches!(result, Err(ClientError::Generic { msg, .. }) if msg.contains("Invalid thumbnail data")));
}

#[test]
fn build_upload_thumbnail_rejects_missing_thumbnail_info() {
let result = build_upload_thumbnail(None, Some(thumbnail_source()));

assert!(matches!(result, Err(ClientError::Generic { msg, .. }) if msg.contains("Invalid thumbnail data")));
}

#[test]
fn build_upload_thumbnail_returns_thumbnail_when_inputs_are_complete() {
let result = build_upload_thumbnail(Some(thumbnail_info()), Some(thumbnail_source()))
.unwrap()
.unwrap();

assert_eq!(result.data, vec![1, 2, 3]);
assert_eq!(result.content_type, "image/jpeg".parse::<Mime>().unwrap());
assert_eq!(result.height, u64_to_uint(60));
assert_eq!(result.width, u64_to_uint(80));
assert_eq!(result.size, u64_to_uint(128));
}
}

impl TryFrom<DraftAttachment> for SdkDraftAttachment {
type Error = ClientError;

Expand Down
73 changes: 73 additions & 0 deletions bindings/matrix-sdk-ffi/src/ruma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ pub enum MessageType {
Location {
content: LocationContent,
},
Custom {
msgtype: String,
body: String,
data_json: String,
},
Other {
msgtype: String,
body: String,
Expand Down Expand Up @@ -422,6 +427,10 @@ impl TryFrom<MessageType> for RumaMessageType {
MessageType::Location { content } => {
Self::Location(RumaLocationMessageEventContent::new(content.body, content.geo_uri))
}
MessageType::Custom { msgtype, body, data_json } => {
let data: JsonObject = serde_json::from_str(&data_json)?;
Self::new(&msgtype, body, data)?
}
MessageType::Other { msgtype, body } => {
Self::new(&msgtype, body, JsonObject::default())?
}
Expand Down Expand Up @@ -471,6 +480,11 @@ impl TryFrom<RumaMessageType> for MessageType {
},
}
}
RumaMessageType::_Custom(_) => MessageType::Custom {
msgtype: value.msgtype().to_owned(),
body: value.body().to_owned(),
data_json: serde_json::to_string(&value.data())?,
},
_ => MessageType::Other {
msgtype: value.msgtype().to_owned(),
body: value.body().to_owned(),
Expand All @@ -479,6 +493,65 @@ impl TryFrom<RumaMessageType> for MessageType {
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::*;

#[test]
fn custom_message_type_from_ruma_preserves_data_json() {
let data = serde_json::from_value(json!({
"org.example.field": "value",
"count": 7,
}))
.unwrap();
let ruma_message_type =
RumaMessageType::new("org.example.custom", "Fallback body".to_owned(), data).unwrap();

let message_type = MessageType::try_from(ruma_message_type).unwrap();

let MessageType::Custom { msgtype, body, data_json } = message_type else {
panic!("expected custom message type");
};

assert_eq!(msgtype, "org.example.custom");
assert_eq!(body, "Fallback body");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&data_json).unwrap(),
json!({
"org.example.field": "value",
"count": 7,
})
);
}

#[test]
fn custom_message_type_to_ruma_restores_data_json() {
let message_type = MessageType::Custom {
msgtype: "org.example.custom".to_owned(),
body: "Fallback body".to_owned(),
data_json: json!({
"org.example.field": "value",
"count": 7,
})
.to_string(),
};

let ruma_message_type = RumaMessageType::try_from(message_type).unwrap();

assert_eq!(ruma_message_type.msgtype(), "org.example.custom");
assert_eq!(ruma_message_type.body(), "Fallback body");
assert_eq!(
serde_json::to_value(ruma_message_type.data()).unwrap(),
json!({
"org.example.field": "value",
"count": 7,
})
);
}
}

#[derive(Clone, uniffi::Enum)]
pub enum RtcNotificationType {
Ring,
Expand Down
1 change: 1 addition & 0 deletions crates/matrix-sdk-ui/src/timeline/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ pub fn default_event_filter(event: &AnySyncTimelineEvent, rules: &RoomVersionRul
| MessageType::Text(_)
| MessageType::Video(_)
| MessageType::VerificationRequest(_) => true,
_ if content.msgtype.msgtype() == "org.letro.letter" => true,
#[cfg(feature = "unstable-msc4274")]
MessageType::Gallery(_) => true,
_ => false,
Expand Down
Loading
Loading