mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-05 06:28:20 -04:00
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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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…
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user