Skip to content

Commit e2148e4

Browse files
committed
feat!(timeline): infer the reply type automatically when sending an attachment
1 parent d1163b7 commit e2148e4

File tree

8 files changed

+176
-98
lines changed

8 files changed

+176
-98
lines changed

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ use eyeball_im::VectorDiff;
2020
use futures_util::pin_mut;
2121
use matrix_sdk::{
2222
attachment::{
23-
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
24-
BaseVideoInfo, Thumbnail,
23+
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
2524
},
2625
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
2726
event_cache::RoomPaginationStatus,
@@ -35,7 +34,7 @@ use matrix_sdk_common::{
3534
stream::StreamExt,
3635
};
3736
use matrix_sdk_ui::timeline::{
38-
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
37+
self, AttachmentConfig, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
3938
TimelineUniqueId as SdkTimelineUniqueId,
4039
};
4140
use mime::Mime;
@@ -111,19 +110,26 @@ impl Timeline {
111110
let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
112111
let mime_type =
113112
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
113+
let replied_to_event_id = params
114+
.in_reply_to
115+
.map(EventId::parse)
116+
.transpose()
117+
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
114118

115119
let formatted_caption = formatted_body_from(
116120
params.caption.as_deref(),
117121
params.formatted_caption.map(Into::into),
118122
);
119123

120-
let attachment_config = AttachmentConfig::new()
121-
.thumbnail(thumbnail)
122-
.info(attachment_info)
123-
.caption(params.caption)
124-
.formatted_caption(formatted_caption)
125-
.mentions(params.mentions.map(Into::into))
126-
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
124+
let attachment_config = AttachmentConfig {
125+
info: Some(attachment_info),
126+
thumbnail,
127+
caption: params.caption,
128+
formatted_caption,
129+
mentions: params.mentions.map(Into::into),
130+
replied_to: replied_to_event_id,
131+
..Default::default()
132+
};
127133

128134
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
129135
let mut request =
@@ -205,8 +211,8 @@ pub struct UploadParameters {
205211
formatted_caption: Option<FormattedBody>,
206212
/// Optional intentional mentions to be sent with the media.
207213
mentions: Option<Mentions>,
208-
/// Optional parameters for sending the media as (threaded) reply.
209-
reply_params: Option<ReplyParameters>,
214+
/// Optional Event ID to reply to.
215+
in_reply_to: Option<String>,
210216
/// Should the media be sent with the send queue, or synchronously?
211217
///
212218
/// Watching progress only works with the synchronous method, at the moment.

crates/matrix-sdk-ui/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ All notable changes to this project will be documented in this file.
88

99
### Features
1010

11+
- [**breaking**] [`Timeline::send_attachment()`] now automatically fills in the thread
12+
relationship, based on the timeline focus. As a result, there's a new
13+
`matrix_sdk_ui::timeline::AttachmentConfig` type in town, that has a simplified optional parameter
14+
`replied_to` of type `OwnedEventId` instead of the `Reply` type and that must be used in place of
15+
`matrix_sdk::attachment::AttachmentConfig`. The proper way to start a thread with a media
16+
attachment is now thus to create a threaded-focused timeline, and then use
17+
`Timeline::send_attachment()`.
18+
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
1119
- [**breaking**] [`Timeline::send_reply()`] now automatically fills in the thread relationship,
1220
based on the timeline focus. As a result, it only takes an `OwnedEventId` parameter, instead of
1321
the `Reply` type. The proper way to start a thread is now thus to create a threaded-focused

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,11 @@ impl<P: RoomDataProvider, D: Decryptor> TimelineController<P, D> {
591591
matches!(&*self.focus, TimelineFocusKind::Live { .. })
592592
}
593593

594+
/// Is this timeline focused on a thread?
595+
pub(super) fn is_threaded(&self) -> bool {
596+
matches!(&*self.focus, TimelineFocusKind::Thread { .. })
597+
}
598+
594599
pub(super) fn thread_root(&self) -> Option<OwnedEventId> {
595600
as_variant!(&*self.focus, TimelineFocusKind::Thread { root_event_id } => root_event_id.clone())
596601
}

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::future::IntoFuture;
22

33
use eyeball::SharedObservable;
4-
use matrix_sdk::{TransmissionProgress, attachment::AttachmentConfig};
4+
use matrix_sdk::TransmissionProgress;
55
use matrix_sdk_base::boxed_into_future;
66
use mime::Mime;
77
use tracing::{Instrument as _, Span};
88

99
use super::{AttachmentSource, Error, Timeline};
10+
use crate::timeline::AttachmentConfig;
1011

1112
pub struct SendAttachment<'a> {
1213
timeline: &'a Timeline,
@@ -72,17 +73,32 @@ impl<'a> IntoFuture for SendAttachment<'a> {
7273
let fut = async move {
7374
let (data, filename) = source.try_into_bytes_and_filename()?;
7475

76+
let reply = timeline.infer_reply(config.replied_to).await;
77+
let sdk_config = matrix_sdk::attachment::AttachmentConfig {
78+
txn_id: config.txn_id,
79+
info: config.info,
80+
thumbnail: config.thumbnail,
81+
caption: config.caption,
82+
formatted_caption: config.formatted_caption,
83+
mentions: config.mentions,
84+
reply,
85+
};
86+
7587
if use_send_queue {
76-
let send_queue = timeline.room().send_queue();
77-
let fut = send_queue.send_attachment(filename, mime_type, data, config);
78-
fut.await.map_err(|_| Error::FailedSendingAttachment)?;
88+
timeline
89+
.room()
90+
.send_queue()
91+
.send_attachment(filename, mime_type, data, sdk_config)
92+
.await
93+
.map_err(|_| Error::FailedSendingAttachment)?;
7994
} else {
80-
let fut = timeline
95+
timeline
8196
.room()
82-
.send_attachment(filename, &mime_type, data, config)
97+
.send_attachment(filename, &mime_type, data, sdk_config)
8398
.with_send_progress_observable(send_progress)
84-
.store_in_cache();
85-
fut.await.map_err(|_| Error::FailedSendingAttachment)?;
99+
.store_in_cache()
100+
.await
101+
.map_err(|_| Error::FailedSendingAttachment)?;
86102
}
87103

88104
Ok(())

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

Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,9 @@ use eyeball_im::VectorDiff;
2525
use futures::SendGallery;
2626
use futures_core::Stream;
2727
use imbl::Vector;
28-
#[cfg(feature = "unstable-msc4274")]
29-
use matrix_sdk::attachment::{AttachmentInfo, Thumbnail};
3028
use matrix_sdk::{
3129
Result,
32-
attachment::AttachmentConfig,
30+
attachment::{AttachmentInfo, Thumbnail},
3331
deserialized_responses::TimelineEvent,
3432
event_cache::{EventCacheDropHandles, RoomEventCache},
3533
executor::JoinHandle,
@@ -43,24 +41,19 @@ use matrix_sdk::{
4341
use mime::Mime;
4442
use pinned_events_loader::PinnedEventsRoom;
4543
use ruma::{
46-
EventId, OwnedEventId, UserId,
44+
EventId, OwnedEventId, OwnedTransactionId, UserId,
4745
api::client::receipt::create_receipt::v3::ReceiptType,
4846
events::{
49-
AnyMessageLikeEventContent, AnySyncTimelineEvent,
47+
AnyMessageLikeEventContent, AnySyncTimelineEvent, Mentions,
5048
poll::unstable_start::{NewUnstablePollStartEventContent, UnstablePollStartEventContent},
5149
receipt::{Receipt, ReceiptThread},
5250
room::{
53-
message::{ReplyWithinThread, RoomMessageEventContentWithoutRelation},
51+
message::{FormattedBody, ReplyWithinThread, RoomMessageEventContentWithoutRelation},
5452
pinned_events::RoomPinnedEventsEventContent,
5553
},
5654
},
5755
room_version_rules::RoomVersionRules,
5856
};
59-
#[cfg(feature = "unstable-msc4274")]
60-
use ruma::{
61-
OwnedTransactionId,
62-
events::{Mentions, room::message::FormattedBody},
63-
};
6457
use subscriber::TimelineWithDropHandle;
6558
use thiserror::Error;
6659
use tracing::{instrument, trace, warn};
@@ -176,6 +169,22 @@ pub enum DateDividerMode {
176169
Monthly,
177170
}
178171

172+
/// Configuration for sending an attachment.
173+
///
174+
/// Like [`matrix_sdk::attachment::AttachmentConfig`], but instead of the
175+
/// `reply` field, there's only a `replied_to` event id; it's the timeline
176+
/// deciding to fill the rest of the reply parameters.
177+
#[derive(Debug, Default)]
178+
pub struct AttachmentConfig {
179+
pub txn_id: Option<OwnedTransactionId>,
180+
pub info: Option<AttachmentInfo>,
181+
pub thumbnail: Option<Thumbnail>,
182+
pub caption: Option<String>,
183+
pub formatted_caption: Option<FormattedBody>,
184+
pub mentions: Option<Mentions>,
185+
pub replied_to: Option<OwnedEventId>,
186+
}
187+
179188
impl Timeline {
180189
/// Returns the room for this timeline.
181190
pub fn room(&self) -> &Room {
@@ -290,44 +299,21 @@ impl Timeline {
290299
// thread relation ourselves.
291300
if let AnyMessageLikeEventContent::RoomMessage(ref room_msg_content) = content
292301
&& room_msg_content.relates_to.is_none()
293-
&& let Some(thread_root) = self.controller.thread_root()
302+
&& self.controller.is_threaded()
294303
{
295-
// The latest event id is used for the reply-to fallback, for clients which
296-
// don't handle threads. It should be correctly set to the latest
297-
// event in the thread, which the timeline instance might or might
298-
// not know about; in this case, we do a best effort of filling it, and resort
299-
// to using the thread root if we don't know about any event.
300-
//
301-
// Note: we could trigger a back-pagination if the timeline is empty, and wait
302-
// for the results, if the timeline is too often empty.
303-
let latest_event_id = self
304-
.controller
305-
.items()
304+
let reply = self
305+
.infer_reply(None)
306306
.await
307-
.iter()
308-
.rev()
309-
.find_map(|item| {
310-
if let TimelineItemKind::Event(event) = item.kind() {
311-
event.event_id().map(ToOwned::to_owned)
312-
} else {
313-
None
314-
}
315-
})
316-
.unwrap_or(thread_root);
317-
307+
.expect("a reply will always be set for threaded timelines");
318308
let content = self
319309
.room()
320310
.make_reply_event(
321311
// Note: this `.into()` gets rid of the relation, but we've checked previously
322312
// that the `relates_to` field wasn't set.
323313
room_msg_content.clone().into(),
324-
Reply {
325-
event_id: latest_event_id,
326-
enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
327-
},
314+
reply,
328315
)
329316
.await?;
330-
331317
Ok(self.room().send_queue().send(content.into()).await?)
332318
} else {
333319
// Otherwise, we send the message as is.
@@ -362,19 +348,62 @@ impl Timeline {
362348
content: RoomMessageEventContentWithoutRelation,
363349
replied_to: OwnedEventId,
364350
) -> Result<(), Error> {
365-
let enforce_thread = if self.controller.thread_root().is_some() {
366-
EnforceThread::Threaded(ReplyWithinThread::Yes)
367-
} else {
368-
EnforceThread::MaybeThreaded
369-
};
370-
let content = self
371-
.room()
372-
.make_reply_event(content, Reply { event_id: replied_to, enforce_thread })
373-
.await?;
351+
let reply = self
352+
.infer_reply(Some(replied_to))
353+
.await
354+
.expect("the reply will always be set because we provided a replied-to event id");
355+
let content = self.room().make_reply_event(content, reply).await?;
374356
self.send(content.into()).await?;
375357
Ok(())
376358
}
377359

360+
/// Given a message or media to send, and an optional `replied_to` event,
361+
/// automatically fills the [`Reply`] information based on the current
362+
/// timeline focus.
363+
pub(crate) async fn infer_reply(&self, replied_to: Option<OwnedEventId>) -> Option<Reply> {
364+
// If there's a replied-to event id, the reply is pretty straightforward, and we
365+
// should only infer the `EnforceThread` based on the current focus.
366+
if let Some(replied_to) = replied_to {
367+
let enforce_thread = if self.controller.is_threaded() {
368+
EnforceThread::Threaded(ReplyWithinThread::Yes)
369+
} else {
370+
EnforceThread::MaybeThreaded
371+
};
372+
return Some(Reply { event_id: replied_to, enforce_thread });
373+
}
374+
375+
let thread_root = self.controller.thread_root()?;
376+
377+
// The latest event id is used for the reply-to fallback, for clients which
378+
// don't handle threads. It should be correctly set to the latest
379+
// event in the thread, which the timeline instance might or might
380+
// not know about; in this case, we do a best effort of filling it, and resort
381+
// to using the thread root if we don't know about any event.
382+
//
383+
// Note: we could trigger a back-pagination if the timeline is empty, and wait
384+
// for the results, if the timeline is too often empty.
385+
386+
let latest_event_id = self
387+
.controller
388+
.items()
389+
.await
390+
.iter()
391+
.rev()
392+
.find_map(|item| {
393+
if let TimelineItemKind::Event(event) = item.kind() {
394+
event.event_id().map(ToOwned::to_owned)
395+
} else {
396+
None
397+
}
398+
})
399+
.unwrap_or(thread_root);
400+
401+
Some(Reply {
402+
event_id: latest_event_id,
403+
enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
404+
})
405+
}
406+
378407
/// Edit an event given its [`TimelineEventItemId`] and some new content.
379408
///
380409
/// Only supports events for which [`EventTimelineItem::is_editable()`]

0 commit comments

Comments
 (0)