feat!(timeline): allow sending media as (thread) replies (#4852)

This makes it possible to reply with a media, as part of a thread or not.

Fixes #4835.

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
Johannes Marbach
2025-04-02 14:25:06 +02:00
committed by GitHub
parent c719cd11f3
commit dccd836dc6
13 changed files with 452 additions and 63 deletions

View File

@@ -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,
}

View File

@@ -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<String>,
/// Optional HTML-formatted caption, for clients that support it.
formatted_caption: Option<FormattedBody>,
// Optional intentional mentions to be sent with the media.
/// Optional intentional mentions to be sent with the media.
mentions: Option<Mentions>,
/// Optional parameters for sending the media as (threaded) reply.
reply_params: Option<ReplyParameters>,
/// 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<dyn TimelineListener>) -> Arc<TaskHandle> {

View File

@@ -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

View File

@@ -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(())
}

View File

@@ -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…

View File

@@ -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<String>,
pub(crate) formatted_caption: Option<FormattedBody>,
pub(crate) mentions: Option<Mentions>,
pub(crate) reply: Option<Reply>,
}
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<Reply>) -> Self {
self.reply = reply;
self
}
}

View File

@@ -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<T, E = Error> = std::result::Result<T, E>;
@@ -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 `<code>` in docs across multiple lines

View File

@@ -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<Mentions>,
) -> RoomMessageEventContent {
reply: Option<Reply>,
) -> Result<RoomMessageEventContent> {
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.

View File

@@ -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<AnyMessageLikeEventContent, ReplyError> {
) -> Result<RoomMessageEventContent, ReplyError> {
make_reply_event(
self,
self.room_id(),
@@ -120,7 +125,7 @@ async fn make_reply_event<S: EventSource>(
content: RoomMessageEventContentWithoutRelation,
event_id: &EventId,
enforce_thread: EnforceThread,
) -> Result<AnyMessageLikeEventContent, ReplyError> {
) -> Result<RoomMessageEventContent, ReplyError> {
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<S: EventSource>(
}
};
Ok(content.into())
Ok(content)
}
async fn replied_to_info_from_event_id<S: EventSource>(
@@ -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);

View File

@@ -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.

View File

@@ -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();

View File

@@ -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;

View File

@@ -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");