Skip to content

Commit dccd836

Browse files
authored
feat!(timeline): allow sending media as (thread) replies (matrix-org#4852)
This makes it possible to reply with a media, as part of a thread or not. Fixes matrix-org#4835. --------- Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
1 parent c719cd1 commit dccd836

File tree

13 files changed

+452
-63
lines changed

13 files changed

+452
-63
lines changed

bindings/matrix-sdk-ffi/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ pub enum RoomError {
288288
TimelineUnavailable,
289289
#[error("Invalid thumbnail data")]
290290
InvalidThumbnailData,
291+
#[error("Invalid replied to event ID")]
292+
InvalidRepliedToEventId,
291293
#[error("Failed sending attachment")]
292294
FailedSendingAttachment,
293295
}

bindings/matrix-sdk-ffi/src/timeline/mod.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use futures_util::{pin_mut, StreamExt as _};
2323
use matrix_sdk::{
2424
attachment::{
2525
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
26-
BaseVideoInfo, Thumbnail,
26+
BaseVideoInfo, Reply, Thumbnail,
2727
},
2828
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
2929
event_cache::RoomPaginationStatus,
@@ -116,12 +116,31 @@ impl Timeline {
116116
params.formatted_caption.map(Into::into),
117117
);
118118

119+
let reply = if let Some(reply_params) = params.reply_params {
120+
let event_id = EventId::parse(reply_params.event_id)
121+
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
122+
let enforce_thread = if reply_params.enforce_thread {
123+
EnforceThread::Threaded(if reply_params.reply_within_thread {
124+
ReplyWithinThread::Yes
125+
} else {
126+
ReplyWithinThread::No
127+
})
128+
} else {
129+
EnforceThread::MaybeThreaded
130+
};
131+
132+
Some(Reply { event_id, enforce_thread })
133+
} else {
134+
None
135+
};
136+
119137
let attachment_config = AttachmentConfig::new()
120138
.thumbnail(thumbnail)
121139
.info(attachment_info)
122140
.caption(params.caption)
123141
.formatted_caption(formatted_caption)
124-
.mentions(params.mentions.map(Into::into));
142+
.mentions(params.mentions.map(Into::into))
143+
.reply(reply);
125144

126145
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
127146
let mut request =
@@ -201,14 +220,27 @@ pub struct UploadParameters {
201220
caption: Option<String>,
202221
/// Optional HTML-formatted caption, for clients that support it.
203222
formatted_caption: Option<FormattedBody>,
204-
// Optional intentional mentions to be sent with the media.
223+
/// Optional intentional mentions to be sent with the media.
205224
mentions: Option<Mentions>,
225+
/// Optional parameters for sending the media as (threaded) reply.
226+
reply_params: Option<ReplyParameters>,
206227
/// Should the media be sent with the send queue, or synchronously?
207228
///
208229
/// Watching progress only works with the synchronous method, at the moment.
209230
use_send_queue: bool,
210231
}
211232

233+
#[derive(uniffi::Record)]
234+
pub struct ReplyParameters {
235+
/// The ID of the event to reply to.
236+
event_id: String,
237+
/// Whether to enforce a thread relation.
238+
enforce_thread: bool,
239+
/// If enforcing a threaded relation, whether the message is a reply on a
240+
/// thread.
241+
reply_within_thread: bool,
242+
}
243+
212244
#[matrix_sdk_ffi_macros::export]
213245
impl Timeline {
214246
pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {

crates/matrix-sdk-ui/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ All notable changes to this project will be documented in this file.
1717
[`Timeline::send_reply()`] now takes an event ID rather than a `RepliedToInfo`.
1818
`Timeline::replied_to_info_from_event_id` has been made private in `matrix_sdk`.
1919
([4842](https://github.com/matrix-org/matrix-rust-sdk/pull/4842))
20+
- Allow sending media as (thread) replies. The reply behaviour can be configured
21+
through new fields on [`AttachmentConfig`].
22+
([4852](https://github.com/matrix-org/matrix-rust-sdk/pull/4852))
2023

2124
### Refactor
2225

crates/matrix-sdk-ui/src/timeline/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ impl Timeline {
282282
enforce_thread: EnforceThread,
283283
) -> Result<(), Error> {
284284
let content = self.room().make_reply_event(content, &event_id, enforce_thread).await?;
285-
self.send(content).await?;
285+
self.send(content.into()).await?;
286286
Ok(())
287287
}
288288

crates/matrix-sdk-ui/tests/integration/timeline/media.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@ use assert_matches2::assert_let;
1919
use eyeball_im::VectorDiff;
2020
use futures_util::{FutureExt, StreamExt};
2121
use matrix_sdk::{
22-
assert_let_timeout, attachment::AttachmentConfig, test_utils::mocks::MatrixMockServer,
22+
assert_let_timeout,
23+
attachment::{AttachmentConfig, Reply},
24+
room::reply::EnforceThread,
25+
test_utils::mocks::MatrixMockServer,
2326
};
2427
use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE};
2528
use matrix_sdk_ui::timeline::{AttachmentSource, EventSendState, RoomExt};
2629
use ruma::{
2730
event_id,
28-
events::room::{message::MessageType, MediaSource},
31+
events::room::{
32+
message::{MessageType, ReplyWithinThread},
33+
MediaSource,
34+
},
2935
room_id,
3036
};
3137
use serde_json::json;
@@ -67,10 +73,12 @@ async fn test_send_attachment_from_file() {
6773

6874
assert!(items.is_empty());
6975

76+
let event_id = event_id!("$event");
7077
let f = EventFactory::new();
7178
mock.sync_room(
7279
&client,
73-
JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("hello").sender(&ALICE)),
80+
JoinedRoomBuilder::new(room_id)
81+
.add_timeline_event(f.text_msg("hello").sender(&ALICE).event_id(event_id)),
7482
)
7583
.await;
7684

@@ -99,7 +107,10 @@ async fn test_send_attachment_from_file() {
99107
mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await;
100108

101109
// Queue sending of an attachment.
102-
let config = AttachmentConfig::new().caption(Some("caption".to_owned()));
110+
let config = AttachmentConfig::new().caption(Some("caption".to_owned())).reply(Some(Reply {
111+
event_id: event_id.to_owned(),
112+
enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
113+
}));
103114
timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap();
104115

105116
{
@@ -115,6 +126,10 @@ async fn test_send_attachment_from_file() {
115126
assert_let!(MessageType::File(file) = msg.msgtype());
116127
assert_let!(MediaSource::Plain(uri) = &file.source);
117128
assert!(uri.to_string().contains("localhost"));
129+
130+
// The message should be considered part of the thread.
131+
let aggregated = item.content().as_msglike().unwrap();
132+
assert!(aggregated.is_threaded());
118133
}
119134

120135
// Eventually, the media is updated with the final MXC IDs…

crates/matrix-sdk/src/attachment.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ use ruma::{
2525
},
2626
Mentions,
2727
},
28-
OwnedTransactionId, TransactionId, UInt,
28+
OwnedEventId, OwnedTransactionId, TransactionId, UInt,
2929
};
3030

31+
use crate::room::reply::EnforceThread;
32+
3133
/// Base metadata about an image.
3234
#[derive(Debug, Clone, Default)]
3335
pub struct BaseImageInfo {
@@ -179,6 +181,15 @@ impl Thumbnail {
179181
}
180182
}
181183

184+
/// Information needed to reply to an event.
185+
#[derive(Debug)]
186+
pub struct Reply {
187+
/// The event ID of the event to reply to.
188+
pub event_id: OwnedEventId,
189+
/// Whether to enforce a thread relation.
190+
pub enforce_thread: EnforceThread,
191+
}
192+
182193
/// Configuration for sending an attachment.
183194
#[derive(Debug, Default)]
184195
pub struct AttachmentConfig {
@@ -188,6 +199,7 @@ pub struct AttachmentConfig {
188199
pub(crate) caption: Option<String>,
189200
pub(crate) formatted_caption: Option<FormattedBody>,
190201
pub(crate) mentions: Option<Mentions>,
202+
pub(crate) reply: Option<Reply>,
191203
}
192204

193205
impl AttachmentConfig {
@@ -262,4 +274,14 @@ impl AttachmentConfig {
262274
self.mentions = mentions;
263275
self
264276
}
277+
278+
/// Set the reply information of the message.
279+
///
280+
/// # Arguments
281+
///
282+
/// * `reply` - The reply information of the message
283+
pub fn reply(mut self, reply: Option<Reply>) -> Self {
284+
self.reply = reply;
285+
self
286+
}
265287
}

crates/matrix-sdk/src/error.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ use serde_json::Error as JsonError;
4545
use thiserror::Error;
4646
use url::ParseError as UrlParseError;
4747

48-
use crate::{event_cache::EventCacheError, media::MediaError, store_locks::LockStoreError};
48+
use crate::{
49+
event_cache::EventCacheError, media::MediaError, room::reply::ReplyError,
50+
store_locks::LockStoreError,
51+
};
4952

5053
/// Result type of the matrix-sdk.
5154
pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -381,6 +384,10 @@ pub enum Error {
381384
/// An error happened during handling of a media subrequest.
382385
#[error(transparent)]
383386
Media(#[from] MediaError),
387+
388+
/// An error happened while attempting to reply to an event.
389+
#[error(transparent)]
390+
ReplyError(#[from] ReplyError),
384391
}
385392

386393
#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines

crates/matrix-sdk/src/room/mod.rs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ pub use self::{
138138
#[cfg(doc)]
139139
use crate::event_cache::EventCache;
140140
use crate::{
141-
attachment::{AttachmentConfig, AttachmentInfo},
141+
attachment::{AttachmentConfig, AttachmentInfo, Reply},
142142
client::WeakClient,
143143
config::RequestConfig,
144144
error::{BeaconError, WrongRoomState},
@@ -2142,18 +2142,21 @@ impl Room {
21422142
}
21432143
}
21442144

2145-
let content = Self::make_attachment_event(
2146-
self.make_attachment_type(
2147-
content_type,
2148-
filename,
2149-
media_source,
2150-
config.caption,
2151-
config.formatted_caption,
2152-
config.info,
2153-
thumbnail,
2154-
),
2155-
mentions,
2156-
);
2145+
let content = self
2146+
.make_attachment_event(
2147+
self.make_attachment_type(
2148+
content_type,
2149+
filename,
2150+
media_source,
2151+
config.caption,
2152+
config.formatted_caption,
2153+
config.info,
2154+
thumbnail,
2155+
),
2156+
mentions,
2157+
config.reply,
2158+
)
2159+
.await?;
21572160

21582161
let mut fut = self.send(content);
21592162
if let Some(txn_id) = txn_id {
@@ -2254,17 +2257,26 @@ impl Room {
22542257
}
22552258
}
22562259

2257-
/// Creates the [`RoomMessageEventContent`] based on the message type and
2258-
/// mentions.
2259-
pub(crate) fn make_attachment_event(
2260+
/// Creates the [`RoomMessageEventContent`] based on the message type,
2261+
/// mentions and reply information.
2262+
pub(crate) async fn make_attachment_event(
2263+
&self,
22602264
msg_type: MessageType,
22612265
mentions: Option<Mentions>,
2262-
) -> RoomMessageEventContent {
2266+
reply: Option<Reply>,
2267+
) -> Result<RoomMessageEventContent> {
22632268
let mut content = RoomMessageEventContent::new(msg_type);
22642269
if let Some(mentions) = mentions {
22652270
content = content.add_mentions(mentions);
22662271
}
2267-
content
2272+
if let Some(reply) = reply {
2273+
// Since we just created the event, there is no relation attached to it. Thus,
2274+
// it is safe to add the reply relation without overriding anything.
2275+
content = self
2276+
.make_reply_event(content.into(), &reply.event_id, reply.enforce_thread)
2277+
.await?;
2278+
}
2279+
Ok(content)
22682280
}
22692281

22702282
/// Update the power levels of a select set of users of this room.

0 commit comments

Comments
 (0)