diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index 4b7d1c241..a1b927ef4 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -288,6 +288,8 @@ pub enum RoomError { TimelineUnavailable, #[error("Invalid thumbnail data")] InvalidThumbnailData, + #[error("Invalid replied to event ID")] + InvalidRepliedToEventId, #[error("Failed sending attachment")] FailedSendingAttachment, } diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 66742056d..8275df3db 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -23,7 +23,7 @@ use futures_util::{pin_mut, StreamExt as _}; use matrix_sdk::{ attachment::{ AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, - BaseVideoInfo, Thumbnail, + BaseVideoInfo, Reply, Thumbnail, }, deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode}, event_cache::RoomPaginationStatus, @@ -116,12 +116,31 @@ impl Timeline { params.formatted_caption.map(Into::into), ); + let reply = if let Some(reply_params) = params.reply_params { + let event_id = EventId::parse(reply_params.event_id) + .map_err(|_| RoomError::InvalidRepliedToEventId)?; + let enforce_thread = if reply_params.enforce_thread { + EnforceThread::Threaded(if reply_params.reply_within_thread { + ReplyWithinThread::Yes + } else { + ReplyWithinThread::No + }) + } else { + EnforceThread::MaybeThreaded + }; + + Some(Reply { event_id, enforce_thread }) + } else { + None + }; + let attachment_config = AttachmentConfig::new() .thumbnail(thumbnail) .info(attachment_info) .caption(params.caption) .formatted_caption(formatted_caption) - .mentions(params.mentions.map(Into::into)); + .mentions(params.mentions.map(Into::into)) + .reply(reply); let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move { let mut request = @@ -201,14 +220,27 @@ pub struct UploadParameters { caption: Option, /// Optional HTML-formatted caption, for clients that support it. formatted_caption: Option, - // Optional intentional mentions to be sent with the media. + /// Optional intentional mentions to be sent with the media. mentions: Option, + /// Optional parameters for sending the media as (threaded) reply. + reply_params: Option, /// Should the media be sent with the send queue, or synchronously? /// /// Watching progress only works with the synchronous method, at the moment. use_send_queue: bool, } +#[derive(uniffi::Record)] +pub struct ReplyParameters { + /// The ID of the event to reply to. + event_id: String, + /// Whether to enforce a thread relation. + enforce_thread: bool, + /// If enforcing a threaded relation, whether the message is a reply on a + /// thread. + reply_within_thread: bool, +} + #[matrix_sdk_ffi_macros::export] impl Timeline { pub async fn add_listener(&self, listener: Box) -> Arc { diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index c7d7f5849..1dc175390 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -17,6 +17,9 @@ All notable changes to this project will be documented in this file. [`Timeline::send_reply()`] now takes an event ID rather than a `RepliedToInfo`. `Timeline::replied_to_info_from_event_id` has been made private in `matrix_sdk`. ([4842](https://github.com/matrix-org/matrix-rust-sdk/pull/4842)) +- Allow sending media as (thread) replies. The reply behaviour can be configured + through new fields on [`AttachmentConfig`]. + ([4852](https://github.com/matrix-org/matrix-rust-sdk/pull/4852)) ### Refactor diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index a2c0b1077..46e743dc9 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -282,7 +282,7 @@ impl Timeline { enforce_thread: EnforceThread, ) -> Result<(), Error> { let content = self.room().make_reply_event(content, &event_id, enforce_thread).await?; - self.send(content).await?; + self.send(content.into()).await?; Ok(()) } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs index c56575420..695988dff 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs @@ -19,13 +19,19 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::{FutureExt, StreamExt}; use matrix_sdk::{ - assert_let_timeout, attachment::AttachmentConfig, test_utils::mocks::MatrixMockServer, + assert_let_timeout, + attachment::{AttachmentConfig, Reply}, + room::reply::EnforceThread, + test_utils::mocks::MatrixMockServer, }; use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE}; use matrix_sdk_ui::timeline::{AttachmentSource, EventSendState, RoomExt}; use ruma::{ event_id, - events::room::{message::MessageType, MediaSource}, + events::room::{ + message::{MessageType, ReplyWithinThread}, + MediaSource, + }, room_id, }; use serde_json::json; @@ -67,10 +73,12 @@ async fn test_send_attachment_from_file() { assert!(items.is_empty()); + let event_id = event_id!("$event"); let f = EventFactory::new(); mock.sync_room( &client, - JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("hello").sender(&ALICE)), + JoinedRoomBuilder::new(room_id) + .add_timeline_event(f.text_msg("hello").sender(&ALICE).event_id(event_id)), ) .await; @@ -99,7 +107,10 @@ async fn test_send_attachment_from_file() { mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await; // Queue sending of an attachment. - let config = AttachmentConfig::new().caption(Some("caption".to_owned())); + let config = AttachmentConfig::new().caption(Some("caption".to_owned())).reply(Some(Reply { + event_id: event_id.to_owned(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + })); timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap(); { @@ -115,6 +126,10 @@ async fn test_send_attachment_from_file() { assert_let!(MessageType::File(file) = msg.msgtype()); assert_let!(MediaSource::Plain(uri) = &file.source); assert!(uri.to_string().contains("localhost")); + + // The message should be considered part of the thread. + let aggregated = item.content().as_msglike().unwrap(); + assert!(aggregated.is_threaded()); } // Eventually, the media is updated with the final MXC IDs… diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index c39f00e30..910f2e36a 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -25,9 +25,11 @@ use ruma::{ }, Mentions, }, - OwnedTransactionId, TransactionId, UInt, + OwnedEventId, OwnedTransactionId, TransactionId, UInt, }; +use crate::room::reply::EnforceThread; + /// Base metadata about an image. #[derive(Debug, Clone, Default)] pub struct BaseImageInfo { @@ -179,6 +181,15 @@ impl Thumbnail { } } +/// Information needed to reply to an event. +#[derive(Debug)] +pub struct Reply { + /// The event ID of the event to reply to. + pub event_id: OwnedEventId, + /// Whether to enforce a thread relation. + pub enforce_thread: EnforceThread, +} + /// Configuration for sending an attachment. #[derive(Debug, Default)] pub struct AttachmentConfig { @@ -188,6 +199,7 @@ pub struct AttachmentConfig { pub(crate) caption: Option, pub(crate) formatted_caption: Option, pub(crate) mentions: Option, + pub(crate) reply: Option, } impl AttachmentConfig { @@ -262,4 +274,14 @@ impl AttachmentConfig { 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 + } } diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 7be7f7341..c2c2b97d6 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -45,7 +45,10 @@ use serde_json::Error as JsonError; use thiserror::Error; use url::ParseError as UrlParseError; -use crate::{event_cache::EventCacheError, media::MediaError, store_locks::LockStoreError}; +use crate::{ + event_cache::EventCacheError, media::MediaError, room::reply::ReplyError, + store_locks::LockStoreError, +}; /// Result type of the matrix-sdk. pub type Result = std::result::Result; @@ -381,6 +384,10 @@ pub enum Error { /// An error happened during handling of a media subrequest. #[error(transparent)] Media(#[from] MediaError), + + /// An error happened while attempting to reply to an event. + #[error(transparent)] + ReplyError(#[from] ReplyError), } #[rustfmt::skip] // stop rustfmt breaking the `` in docs across multiple lines diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 56a53bd0d..e5c6a26a6 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -138,7 +138,7 @@ pub use self::{ #[cfg(doc)] use crate::event_cache::EventCache; use crate::{ - attachment::{AttachmentConfig, AttachmentInfo}, + attachment::{AttachmentConfig, AttachmentInfo, Reply}, client::WeakClient, config::RequestConfig, error::{BeaconError, WrongRoomState}, @@ -2142,18 +2142,21 @@ impl Room { } } - let content = Self::make_attachment_event( - self.make_attachment_type( - content_type, - filename, - media_source, - config.caption, - config.formatted_caption, - config.info, - thumbnail, - ), - mentions, - ); + let content = self + .make_attachment_event( + self.make_attachment_type( + content_type, + filename, + media_source, + config.caption, + config.formatted_caption, + config.info, + thumbnail, + ), + mentions, + config.reply, + ) + .await?; let mut fut = self.send(content); if let Some(txn_id) = txn_id { @@ -2254,17 +2257,26 @@ impl Room { } } - /// Creates the [`RoomMessageEventContent`] based on the message type and - /// mentions. - pub(crate) fn make_attachment_event( + /// Creates the [`RoomMessageEventContent`] based on the message type, + /// mentions and reply information. + pub(crate) async fn make_attachment_event( + &self, msg_type: MessageType, mentions: Option, - ) -> RoomMessageEventContent { + reply: Option, + ) -> Result { let mut content = RoomMessageEventContent::new(msg_type); if let Some(mentions) = mentions { content = content.add_mentions(mentions); } - content + if let Some(reply) = reply { + // Since we just created the event, there is no relation attached to it. Thus, + // it is safe to add the reply relation without overriding anything. + content = self + .make_reply_event(content.into(), &reply.event_id, reply.enforce_thread) + .await?; + } + Ok(content) } /// Update the power levels of a select set of users of this room. diff --git a/crates/matrix-sdk/src/room/reply.rs b/crates/matrix-sdk/src/room/reply.rs index fcb81b5bf..e1815afed 100644 --- a/crates/matrix-sdk/src/room/reply.rs +++ b/crates/matrix-sdk/src/room/reply.rs @@ -24,8 +24,7 @@ use ruma::{ RoomMessageEventContent, RoomMessageEventContentWithoutRelation, }, }, - AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, - SyncMessageLikeEvent, + AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent, }, serde::Raw, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UserId, @@ -94,13 +93,19 @@ impl Room { /// /// The event can then be sent with [`Room::send`] or a /// [`crate::send_queue::RoomSendQueue`]. + /// + /// # Arguments + /// + /// * `content` - The content to reply with + /// * `event_id` - ID of the event to reply to + /// * `enforce_thread` - Whether to enforce a thread relation #[instrument(skip(self, content), fields(room = %self.room_id()))] pub async fn make_reply_event( &self, content: RoomMessageEventContentWithoutRelation, event_id: &EventId, enforce_thread: EnforceThread, - ) -> Result { + ) -> Result { make_reply_event( self, self.room_id(), @@ -120,7 +125,7 @@ async fn make_reply_event( content: RoomMessageEventContentWithoutRelation, event_id: &EventId, enforce_thread: EnforceThread, -) -> Result { +) -> Result { let replied_to_info = replied_to_info_from_event_id(source, event_id).await?; // [The specification](https://spec.matrix.org/v1.10/client-server-api/#user-and-room-mentions) says: @@ -222,7 +227,7 @@ async fn make_reply_event( } }; - Ok(content.into()) + Ok(content) } async fn replied_to_info_from_event_id( @@ -267,7 +272,7 @@ mod tests { event_id, events::{ room::message::{Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation}, - AnyMessageLikeEventContent, AnySyncTimelineEvent, + AnySyncTimelineEvent, }, room_id, serde::Raw, @@ -419,8 +424,7 @@ mod tests { .await .unwrap(); - assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event); - assert_let!(Some(Relation::Reply { in_reply_to }) = &msg.relates_to); + assert_let!(Some(Relation::Reply { in_reply_to }) = &reply_event.relates_to); assert_eq!(in_reply_to.event_id, event_id); } @@ -451,8 +455,7 @@ mod tests { .await .unwrap(); - assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event); - assert_let!(Some(Relation::Thread(thread)) = &msg.relates_to); + assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to); assert_eq!(thread.event_id, event_id); assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id); @@ -494,8 +497,7 @@ mod tests { .await .unwrap(); - assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event); - assert_let!(Some(Relation::Thread(thread)) = &msg.relates_to); + assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to); assert_eq!(thread.event_id, thread_root); assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id); @@ -537,8 +539,7 @@ mod tests { .await .unwrap(); - assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event); - assert_let!(Some(Relation::Thread(thread)) = &msg.relates_to); + assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to); assert_eq!(thread.event_id, thread_root); assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id); @@ -580,8 +581,7 @@ mod tests { .await .unwrap(); - assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event); - assert_let!(Some(Relation::Thread(thread)) = &msg.relates_to); + assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to); assert_eq!(thread.event_id, thread_root); assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id); diff --git a/crates/matrix-sdk/src/send_queue/mod.rs b/crates/matrix-sdk/src/send_queue/mod.rs index 4469227cf..9379c4249 100644 --- a/crates/matrix-sdk/src/send_queue/mod.rs +++ b/crates/matrix-sdk/src/send_queue/mod.rs @@ -1841,6 +1841,10 @@ pub enum RoomSendQueueError { /// Error coming from storage. #[error(transparent)] StorageError(#[from] RoomSendQueueStorageError), + + /// The attachment event failed to be created. + #[error("the attachment event could not be created")] + FailedToCreateAttachment, } /// An error triggered by the send queue storage. diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 479e94034..5e281dff0 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -41,7 +41,7 @@ use crate::{ LocalEcho, LocalEchoContent, MediaHandles, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle, }, - Client, Media, Room, + Client, Media, }; /// Replace the source by the final ones in all the media types handled by @@ -183,18 +183,22 @@ impl RoomSendQueue { }; // Create the content for the media event. - let event_content = Room::make_attachment_event( - room.make_attachment_type( - &content_type, - filename, - file_media_request.source.clone(), - config.caption, - config.formatted_caption, - config.info, - event_thumbnail_info, - ), - config.mentions, - ); + let event_content = room + .make_attachment_event( + room.make_attachment_type( + &content_type, + filename, + file_media_request.source.clone(), + config.caption, + config.formatted_caption, + config.info, + event_thumbnail_info, + ), + config.mentions, + config.reply, + ) + .await + .map_err(|_| RoomSendQueueError::FailedToCreateAttachment)?; let created_at = MilliSecondsSinceUnixEpoch::now(); diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index 7ef75d97c..89f105cca 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -1,14 +1,20 @@ use std::time::Duration; use matrix_sdk::{ - attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseVideoInfo, Thumbnail}, + attachment::{ + AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseVideoInfo, Reply, Thumbnail, + }, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, + room::reply::EnforceThread, test_utils::mocks::MatrixMockServer, }; -use matrix_sdk_test::{async_test, DEFAULT_TEST_ROOM_ID}; +use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID}; use ruma::{ event_id, - events::{room::MediaSource, Mentions}, + events::{ + room::{message::ReplyWithinThread, MediaSource}, + Mentions, + }, mxc_uri, owned_mxc_uri, owned_user_id, uint, }; use serde_json::json; @@ -297,6 +303,262 @@ async fn test_room_attachment_send_mentions() { assert_eq!(expected_event_id, response.event_id); } +#[async_test] +async fn test_room_attachment_reply_outside_thread() { + let mock = MatrixMockServer::new().await; + + let expected_event_id = event_id!("$h29iv0s8:example.com"); + let replied_to_event_id = event_id!("$foo:bar.com"); + + mock.mock_room_send() + .body_matches_partial_json(json!({ + "m.relates_to": { + "m.in_reply_to": { + "event_id": replied_to_event_id + }, + } + })) + .ok(expected_event_id) + .mock_once() + .mount() + .await; + + let f = EventFactory::new(); + mock.mock_room_event() + .match_event_id() + .ok(f + .text_msg("Send me your attachments") + .sender(*ALICE) + .event_id(replied_to_event_id) + .into()) + .mock_once() + .mount() + .await; + + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() + .await; + + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + b"Hello world".to_vec(), + AttachmentConfig::new() + .mentions(Some(Mentions::with_user_ids([owned_user_id!("@user:localhost")]))) + .reply(Some(Reply { + event_id: replied_to_event_id.into(), + enforce_thread: EnforceThread::Unthreaded, + })), + ) + .await + .unwrap(); + + assert_eq!(expected_event_id, response.event_id); +} + +#[async_test] +async fn test_room_attachment_start_thread() { + let mock = MatrixMockServer::new().await; + + let expected_event_id = event_id!("$h29iv0s8:example.com"); + let replied_to_event_id = event_id!("$foo:bar.com"); + + mock.mock_room_send() + .body_matches_partial_json(json!({ + "m.relates_to": { + "rel_type": "m.thread", + "event_id": replied_to_event_id, + "m.in_reply_to": { + "event_id": replied_to_event_id + }, + "is_falling_back": true + }, + })) + .ok(expected_event_id) + .mock_once() + .mount() + .await; + + let f = EventFactory::new(); + mock.mock_room_event() + .match_event_id() + .ok(f + .text_msg("Send me your attachments") + .sender(*ALICE) + .event_id(replied_to_event_id) + .into()) + .mock_once() + .mount() + .await; + + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() + .await; + + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + b"Hello world".to_vec(), + AttachmentConfig::new() + .mentions(Some(Mentions::with_user_ids([owned_user_id!("@user:localhost")]))) + .reply(Some(Reply { + event_id: replied_to_event_id.into(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + })), + ) + .await + .unwrap(); + + assert_eq!(expected_event_id, response.event_id); +} + +#[async_test] +async fn test_room_attachment_reply_on_thread_as_reply() { + let mock = MatrixMockServer::new().await; + + let expected_event_id = event_id!("$h29iv0s8:example.com"); + let thread_root_event_id = event_id!("$bar:foo.com"); + let replied_to_event_id = event_id!("$foo:bar.com"); + + mock.mock_room_send() + .body_matches_partial_json(json!({ + "m.relates_to": { + "rel_type": "m.thread", + "event_id": thread_root_event_id, + "m.in_reply_to": { + "event_id": replied_to_event_id + }, + }, + })) + .ok(expected_event_id) + .mock_once() + .mount() + .await; + + let f = EventFactory::new(); + mock.mock_room_event() + .match_event_id() + .ok(f + .text_msg("Send me your attachments") + .sender(*ALICE) + .event_id(replied_to_event_id) + .in_thread(thread_root_event_id, thread_root_event_id) + .into()) + .mock_once() + .mount() + .await; + + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() + .await; + + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + b"Hello world".to_vec(), + AttachmentConfig::new() + .mentions(Some(Mentions::with_user_ids([owned_user_id!("@user:localhost")]))) + .reply(Some(Reply { + event_id: replied_to_event_id.into(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::Yes), + })), + ) + .await + .unwrap(); + + assert_eq!(expected_event_id, response.event_id); +} + +#[async_test] +async fn test_room_attachment_reply_forwarding_thread() { + let mock = MatrixMockServer::new().await; + + let expected_event_id = event_id!("$h29iv0s8:example.com"); + let thread_root_event_id = event_id!("$bar:foo.com"); + let replied_to_event_id = event_id!("$foo:bar.com"); + + mock.mock_room_send() + .body_matches_partial_json(json!({ + "m.relates_to": { + "rel_type": "m.thread", + "event_id": thread_root_event_id, + "m.in_reply_to": { + "event_id": replied_to_event_id + }, + "is_falling_back": true + }, + })) + .ok(expected_event_id) + .mock_once() + .mount() + .await; + + let f = EventFactory::new(); + mock.mock_room_event() + .match_event_id() + .ok(f + .text_msg("Send me your attachments") + .sender(*ALICE) + .event_id(replied_to_event_id) + .in_thread(thread_root_event_id, thread_root_event_id) + .into()) + .mock_once() + .mount() + .await; + + mock.mock_upload() + .expect_mime_type("image/jpeg") + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() + .await; + + let client = mock.client_builder().build().await; + let room = mock.sync_joined_room(&client, &DEFAULT_TEST_ROOM_ID).await; + mock.mock_room_state_encryption().plain().mount().await; + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + b"Hello world".to_vec(), + AttachmentConfig::new() + .mentions(Some(Mentions::with_user_ids([owned_user_id!("@user:localhost")]))) + .reply(Some(Reply { + event_id: replied_to_event_id.into(), + enforce_thread: EnforceThread::MaybeThreaded, + })), + ) + .await + .unwrap(); + + assert_eq!(expected_event_id, response.event_id); +} + #[async_test] async fn test_room_attachment_send_is_animated() { let mock = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 392448736..daf5bf8e3 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -3,7 +3,7 @@ use std::{ops::Not as _, sync::Arc, time::Duration}; use as_variant::as_variant; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ - attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, Thumbnail}, + attachment::{AttachmentConfig, AttachmentInfo, BaseImageInfo, Reply, Thumbnail}, config::StoreConfig, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, send_queue::{ @@ -15,7 +15,7 @@ use matrix_sdk::{ }; use matrix_sdk_test::{ async_test, event_factory::EventFactory, InvitedRoomBuilder, KnockedRoomBuilder, - LeftRoomBuilder, + LeftRoomBuilder, ALICE, }; use ruma::{ event_id, @@ -25,7 +25,10 @@ use ruma::{ UnstablePollStartContentBlock, UnstablePollStartEventContent, }, room::{ - message::{ImageMessageEventContent, MessageType, RoomMessageEventContent}, + message::{ + ImageMessageEventContent, MessageType, Relation, ReplyWithinThread, + RoomMessageEventContent, + }, MediaSource, }, AnyMessageLikeEventContent, EventContent as _, Mentions, @@ -1797,6 +1800,7 @@ async fn test_media_uploads() { let filename = "surprise.jpeg.exe"; let content_type = mime::IMAGE_JPEG; let data = b"hello world".to_vec(); + let replied_to_event_id = event_id!("$foo:bar.com"); let thumbnail = Thumbnail { data: b"thumbnail".to_vec(), @@ -1821,6 +1825,10 @@ async fn test_media_uploads() { .txn_id(&transaction_id) .caption(Some("caption".to_owned())) .mentions(Some(mentions.clone())) + .reply(Some(Reply { + event_id: replied_to_event_id.into(), + enforce_thread: matrix_sdk::room::reply::EnforceThread::Threaded(ReplyWithinThread::No), + })) .info(attachment_info); // ---------------------- @@ -1828,6 +1836,18 @@ async fn test_media_uploads() { mock.mock_room_state_encryption().plain().mount().await; mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + let f = EventFactory::new(); + mock.mock_room_event() + .match_event_id() + .ok(f + .text_msg("Send me your attachments") + .sender(*ALICE) + .event_id(replied_to_event_id) + .into()) + .mock_once() + .mount() + .await; + let allow_upload_lock = Arc::new(Mutex::new(())); let block_upload = allow_upload_lock.lock().await; @@ -1860,6 +1880,12 @@ async fn test_media_uploads() { vec![owned_user_id!("@ivan:sdk.rs")] ); + // Check relations. + assert_let!(Some(Relation::Thread(thread)) = content.relates_to); + assert_eq!(thread.event_id, replied_to_event_id); + assert_eq!(thread.in_reply_to.unwrap().event_id, replied_to_event_id); + assert!(thread.is_falling_back); + // Check metadata. assert_let!(MessageType::Image(img_content) = content.msgtype); assert_eq!(img_content.body, "caption");