atagen

macltok

public
Star0
Watch0

Commit 0827ad2d24e9

tarball

atagen <boss@atagen.co>2026-10-23 07:30 UTC

5 files changed+88-27
uploads: carry replies, rich captions and mentions on attachments

malice had to send a reply's text and its files as separate events, so an image-only reply lost its relation and captions dropped their formatting and m.mentions. A reply in a thread is resolved before queueing because the send queue builds the relation itself and the timeline's wrong-thread check never sees it.
Modified file: src/engine/media.rs+57 additions−9 deletions
@@ -23,18 +23,20 @@ use matrix_sdk::{
2323 },
2424 events::room::{
2525 EncryptedFileHash, EncryptedFileHashAlgorithm, EncryptedFileInfo, MediaSource,
26- message::{AddMentions, ReplyWithinThread},
26+ message::{
27+ AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
28+ },
2729 },
2830 },
2931 };
3032 use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
31-use ruma::MxcUri;
33+use ruma::{MxcUri, OwnedEventId};
3234 use tokio::sync::Mutex;
3335
3436 use super::{executor::EngineBoxFuture, url_preview::UrlPreviews};
3537 use crate::{
3638 AttachmentEncryption, AttachmentUpload, Error, ErrorKind, LinkPreview, Media, MediaFile,
37- MediaId, MediaKind, MediaLocation, MediaSize, TimelineId,
39+ MediaId, MediaKind, MediaLocation, MediaSize,
3840 normalise::{MediaRegistration, MediaScope, MediaSourceInfo},
3941 upload::prepare_upload,
4042 };
@@ -655,14 +657,35 @@ pub(crate) async fn queue_upload(
655657 upload: AttachmentUpload,
656658 max_bytes: usize,
657659 ) -> Result<(), Error> {
658- let (upload, mime, mut config) = prepare_upload(upload, max_bytes)?;
659- if let TimelineId::Thread { root_event_id, .. } = &upload.timeline_id {
660- config.reply = Some(SdkReply {
661- event_id: root_event_id.clone(),
660+ let (mut upload, mime, mut config) = prepare_upload(upload, max_bytes)?;
661+ let thread_root = upload.timeline_id.thread_root().cloned();
662+ config.reply = match (upload.reply_to.take(), thread_root) {
663+ (Some(event_id), thread_root) => {
664+ let reply = || SdkReply {
665+ event_id: event_id.clone(),
666+ enforce_thread: if thread_root.is_some() {
667+ EnforceThread::Threaded(ReplyWithinThread::Yes)
668+ } else {
669+ EnforceThread::MaybeThreaded
670+ },
671+ add_mentions: if upload.notify_replied_to {
672+ AddMentions::Yes
673+ } else {
674+ AddMentions::No
675+ },
676+ };
677+ if let Some(thread_root) = &thread_root {
678+ check_reply_thread(&room, reply(), thread_root).await?;
679+ }
680+ Some(reply())
681+ }
682+ (None, Some(root_event_id)) => Some(SdkReply {
683+ event_id: root_event_id,
662684 enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
663685 add_mentions: AddMentions::No,
664- });
665- }
686+ }),
687+ (None, None) => None,
688+ };
666689 room.send_queue()
667690 .send_attachment(upload.filename, mime, upload.bytes, config)
668691 .await
@@ -680,6 +703,31 @@ pub(crate) async fn queue_upload(
680703 })
681704 }
682705
706+/// the send queue builds the relation itself, so a reply that would land in another thread has to be caught before it is queued
707+async fn check_reply_thread(
708+ room: &Room,
709+ reply: SdkReply,
710+ thread_root: &OwnedEventId,
711+) -> Result<(), Error> {
712+ let content = room
713+ .make_reply_event(RoomMessageEventContentWithoutRelation::text_plain(""), reply)
714+ .await
715+ .map_err(|source| {
716+ Error::with_source(ErrorKind::Timeline, "reply operation failed", source)
717+ })?;
718+ if matches!(
719+ content.relates_to.as_ref(),
720+ Some(Relation::Thread(thread)) if &thread.event_id == thread_root
721+ ) {
722+ Ok(())
723+ } else {
724+ Err(Error::new(
725+ ErrorKind::Invalid,
726+ "reply target does not belong to the selected thread",
727+ ))
728+ }
729+}
730+
683731 /// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down
684732 struct MediaRegistry {
685733 entries: HashMap<MediaId, (MediaRegistration, u64)>,
Modified file: src/engine/timeline.rs+4 additions−11 deletions
@@ -7,11 +7,8 @@ use matrix_sdk::{
77 room::reply::{EnforceThread, Reply as SdkReply},
88 ruma::{
99 api::client::receipt::create_receipt::v3::ReceiptType,
10- events::{
11- Mentions,
12- room::message::{
13- AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
14- },
10+ events::room::message::{
11+ AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
1512 },
1613 },
1714 };
@@ -26,6 +23,7 @@ use crate::{
2623 engine::MediaService,
2724 normalise::{MediaScope, normalise_timeline, normalise_timeline_diffs},
2825 transport::{EventSender, Responder, SendEventError},
26+ upload::sdk_mentions,
2927 };
3028
3129 const PAGINATION_QUEUE: usize = 32;
@@ -455,12 +453,7 @@ fn message_content(
455453 None if emote => RoomMessageEventContentWithoutRelation::emote_plain(content.body),
456454 None => RoomMessageEventContentWithoutRelation::text_plain(content.body),
457455 };
458- let content = content.add_mentions(Mentions::with_user_ids(mentions.user_ids));
459- if mentions.room {
460- content.add_mentions(Mentions::with_room_mention())
461- } else {
462- content
463- }
456+ content.add_mentions(sdk_mentions(mentions))
464457 }
465458
466459 pub(super) fn timeline_error(source: matrix_sdk_ui::timeline::Error) -> Error {
Modified file: src/model.rs+7 additions−1 deletions
@@ -334,7 +334,10 @@ pub struct AttachmentUpload {
334334 pub filename: String,
335335 pub mimetype: String,
336336 pub bytes: Vec<u8>,
337- pub caption: Option<String>,
337+ pub caption: Option<RichText>,
338+ pub mentions: MessageMentions,
339+ pub reply_to: Option<OwnedEventId>,
340+ pub notify_replied_to: bool,
338341 pub details: Option<UploadDetails>,
339342 pub thumbnail: Option<UploadThumbnail>,
340343 }
@@ -353,6 +356,9 @@ impl AttachmentUpload {
353356 mimetype: mimetype.into(),
354357 bytes,
355358 caption: None,
359+ mentions: MessageMentions::default(),
360+ reply_to: None,
361+ notify_replied_to: true,
356362 details: None,
357363 thumbnail: None,
358364 }
Modified file: src/upload.rs+17 additions−3 deletions
@@ -5,10 +5,23 @@ use matrix_sdk::{
55 AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
66 BaseVideoInfo, Thumbnail,
77 },
8- ruma::events::room::message::TextMessageEventContent,
8+ ruma::events::{Mentions, room::message::TextMessageEventContent},
99 };
1010
11-use crate::{AttachmentUpload, Error, ErrorKind, UploadDetails};
11+use crate::{AttachmentUpload, Error, ErrorKind, MessageMentions, RichText, UploadDetails};
12+
13+pub(crate) fn sdk_mentions(mentions: MessageMentions) -> Mentions {
14+ let mut sdk = Mentions::with_user_ids(mentions.user_ids);
15+ sdk.room = mentions.room;
16+ sdk
17+}
18+
19+fn caption_content(caption: RichText) -> TextMessageEventContent {
20+ match caption.formatted_body {
21+ Some(formatted_body) => TextMessageEventContent::html(caption.body, formatted_body),
22+ None => TextMessageEventContent::plain(caption.body),
23+ }
24+}
1225
1326 pub(crate) fn prepare_upload(
1427 mut upload: AttachmentUpload,
@@ -163,6 +176,7 @@ pub(crate) fn prepare_upload(
163176 let config = AttachmentConfig::new()
164177 .info(info)
165178 .thumbnail(thumbnail)
166- .caption(upload.caption.clone().map(TextMessageEventContent::plain));
179+ .caption(upload.caption.take().map(caption_content))
180+ .mentions(Some(sdk_mentions(std::mem::take(&mut upload.mentions))));
167181 Ok((upload, mimetype, config))
168182 }
Modified file: tests/media.rs+3 additions−3 deletions
@@ -4,8 +4,8 @@ use std::{sync::Arc, time::Duration};
44
55 use macltok::{
66 AttachmentUpload, ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, Event,
7- EventStream, MediaFile, MediaId, MediaLocation, MediaSize, UploadDetails, UploadOutcome,
8- UploadThumbnail,
7+ EventStream, MediaFile, MediaId, MediaLocation, MediaSize, RichText, UploadDetails,
8+ UploadOutcome, UploadThumbnail,
99 };
1010 use matrix_sdk::{
1111 media::{MediaFormat, MediaRequestParameters},
@@ -397,7 +397,7 @@ async fn media_upload_is_queued_and_sent() {
397397 "image/jpeg",
398398 b"photo data".to_vec(),
399399 );
400- upload.caption = Some("holiday".to_owned());
400+ upload.caption = Some(RichText::plain("holiday"));
401401 harness.handle.upload(upload).await.unwrap();
402402
403403 tokio::time::timeout(Duration::from_secs(5), async {