mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-06 15:04:11 -04:00
room: create edits to add a caption to a media event
This commit is contained in:
@@ -461,6 +461,7 @@ impl Timeline {
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
EditedContent::PollStart { new_content, .. } => {
|
||||
if matches!(item.content, TimelineItemContent::Poll(_)) {
|
||||
AnyMessageLikeEventContent::UnstablePollStart(
|
||||
@@ -476,6 +477,10 @@ impl Timeline {
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
EditedContent::MediaCaption { caption: _, formatted_caption: _ } => {
|
||||
todo!("bnjbvr you had one job");
|
||||
}
|
||||
};
|
||||
|
||||
if !handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)? {
|
||||
|
||||
@@ -23,9 +23,13 @@ use ruma::{
|
||||
ReplacementUnstablePollStartEventContent, UnstablePollStartContentBlock,
|
||||
UnstablePollStartEventContent,
|
||||
},
|
||||
room::message::{Relation, ReplacementMetadata, RoomMessageEventContentWithoutRelation},
|
||||
room::message::{
|
||||
FormattedBody, MessageType, Relation, ReplacementMetadata, RoomMessageEventContent,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
AnyMessageLikeEvent, AnyMessageLikeEventContent, AnySyncMessageLikeEvent,
|
||||
AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEvent, SyncMessageLikeEvent,
|
||||
AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEvent, OriginalMessageLikeEvent,
|
||||
SyncMessageLikeEvent,
|
||||
},
|
||||
EventId, RoomId, UserId,
|
||||
};
|
||||
@@ -39,6 +43,19 @@ pub enum EditedContent {
|
||||
/// The content is a `m.room.message`.
|
||||
RoomMessage(RoomMessageEventContentWithoutRelation),
|
||||
|
||||
/// Tweak a caption for a `m.room.message` that's a media.
|
||||
MediaCaption {
|
||||
/// New caption for the media.
|
||||
///
|
||||
/// Set to `None` to remove an existing caption.
|
||||
caption: Option<String>,
|
||||
|
||||
/// New formatted caption for the media.
|
||||
///
|
||||
/// Set to `None` to remove an existing formatted caption.
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
},
|
||||
|
||||
/// The content is a new poll start.
|
||||
PollStart {
|
||||
/// New fallback text for the poll.
|
||||
@@ -53,6 +70,7 @@ impl std::fmt::Debug for EditedContent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::RoomMessage(_) => f.debug_tuple("RoomMessage").finish(),
|
||||
Self::MediaCaption { .. } => f.debug_tuple("MediaCaption").finish(),
|
||||
Self::PollStart { .. } => f.debug_tuple("PollStart").finish(),
|
||||
}
|
||||
}
|
||||
@@ -133,6 +151,28 @@ impl<'a> EventSource for &'a Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the caption of a media event content.
|
||||
///
|
||||
/// Why a macro over a plain function: the event content types all differ from
|
||||
/// each other, and it would require adding a trait and implementing it for all
|
||||
/// event types instead of having this simple macro.
|
||||
macro_rules! set_caption {
|
||||
($event:expr, $caption:expr) => {
|
||||
let filename = $event.filename().to_owned();
|
||||
// As a reminder:
|
||||
// - body and no filename set means the body is the filename
|
||||
// - body and filename set means the body is the caption, and filename is the
|
||||
// filename.
|
||||
if let Some(caption) = $caption {
|
||||
$event.filename = Some(filename);
|
||||
$event.body = caption;
|
||||
} else {
|
||||
$event.filename = None;
|
||||
$event.body = filename;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async fn make_edit_event<S: EventSource>(
|
||||
source: S,
|
||||
room_id: &RoomId,
|
||||
@@ -167,47 +207,66 @@ async fn make_edit_event<S: EventSource>(
|
||||
};
|
||||
|
||||
let mentions = original.content.mentions.clone();
|
||||
let replied_to_original_room_msg =
|
||||
extract_replied_to(source, room_id, original.content.relates_to).await;
|
||||
|
||||
// Do a best effort at finding the replied-to original event.
|
||||
let replied_to_sync_timeline_event =
|
||||
if let Some(Relation::Reply { in_reply_to }) = original.content.relates_to {
|
||||
source
|
||||
.get_event(&in_reply_to.event_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("couldn't fetch the replied-to event, when editing: {err}");
|
||||
err
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let replacement = new_content.make_replacement(
|
||||
ReplacementMetadata::new(event_id.to_owned(), mentions),
|
||||
replied_to_original_room_msg.as_ref(),
|
||||
);
|
||||
|
||||
let replied_to_original_room_msg = replied_to_sync_timeline_event
|
||||
.and_then(|sync_timeline_event| {
|
||||
sync_timeline_event
|
||||
.raw()
|
||||
.deserialize()
|
||||
.map_err(|err| warn!("unable to deserialize replied-to event: {err}"))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|event| {
|
||||
if let AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(
|
||||
MessageLikeEvent::Original(original),
|
||||
)) = event.into_full_event(room_id.to_owned())
|
||||
{
|
||||
Some(original)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Ok(replacement.into())
|
||||
}
|
||||
|
||||
EditedContent::MediaCaption { caption, formatted_caption } => {
|
||||
// Handle edits of m.room.message.
|
||||
let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original)) =
|
||||
message_like_event
|
||||
else {
|
||||
return Err(EditError::IncompatibleEditType {
|
||||
target: message_like_event.event_type().to_string(),
|
||||
new_content: "caption for a media room message",
|
||||
});
|
||||
};
|
||||
|
||||
Ok(new_content
|
||||
.make_replacement(
|
||||
ReplacementMetadata::new(event_id.to_owned(), mentions),
|
||||
replied_to_original_room_msg.as_ref(),
|
||||
)
|
||||
.into())
|
||||
let mentions = original.content.mentions.clone();
|
||||
let replied_to_original_room_msg =
|
||||
extract_replied_to(source, room_id, original.content.relates_to.clone()).await;
|
||||
|
||||
let mut prev_content = original.content;
|
||||
|
||||
match &mut prev_content.msgtype {
|
||||
MessageType::Audio(event) => {
|
||||
set_caption!(event, caption);
|
||||
event.formatted = formatted_caption;
|
||||
}
|
||||
MessageType::File(event) => {
|
||||
set_caption!(event, caption);
|
||||
event.formatted = formatted_caption;
|
||||
}
|
||||
MessageType::Image(event) => {
|
||||
set_caption!(event, caption);
|
||||
event.formatted = formatted_caption;
|
||||
}
|
||||
MessageType::Video(event) => {
|
||||
set_caption!(event, caption);
|
||||
event.formatted = formatted_caption;
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(EditError::IncompatibleEditType {
|
||||
target: prev_content.msgtype.msgtype().to_owned(),
|
||||
new_content: "caption for a media room message",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let replacement = prev_content.make_replacement(
|
||||
ReplacementMetadata::new(event_id.to_owned(), mentions),
|
||||
replied_to_original_room_msg.as_ref(),
|
||||
);
|
||||
|
||||
Ok(replacement.into())
|
||||
}
|
||||
|
||||
EditedContent::PollStart { fallback_text, new_content } => {
|
||||
@@ -234,6 +293,45 @@ async fn make_edit_event<S: EventSource>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to find the original replied-to event content, in a best-effort manner.
|
||||
async fn extract_replied_to<S: EventSource>(
|
||||
source: S,
|
||||
room_id: &RoomId,
|
||||
relates_to: Option<Relation<RoomMessageEventContentWithoutRelation>>,
|
||||
) -> Option<OriginalMessageLikeEvent<RoomMessageEventContent>> {
|
||||
let replied_to_sync_timeline_event = if let Some(Relation::Reply { in_reply_to }) = relates_to {
|
||||
source
|
||||
.get_event(&in_reply_to.event_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("couldn't fetch the replied-to event, when editing: {err}");
|
||||
err
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
replied_to_sync_timeline_event
|
||||
.and_then(|sync_timeline_event| {
|
||||
sync_timeline_event
|
||||
.raw()
|
||||
.deserialize()
|
||||
.map_err(|err| warn!("unable to deserialize replied-to event: {err}"))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|event| {
|
||||
if let AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(
|
||||
MessageLikeEvent::Original(original),
|
||||
)) = event.into_full_event(room_id.to_owned())
|
||||
{
|
||||
Some(original)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
@@ -244,10 +342,10 @@ mod tests {
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{
|
||||
room::message::{Relation, RoomMessageEventContentWithoutRelation},
|
||||
room::message::{MessageType, Relation, RoomMessageEventContentWithoutRelation},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
room_id,
|
||||
owned_mxc_uri, room_id,
|
||||
serde::Raw,
|
||||
user_id, EventId, OwnedEventId,
|
||||
};
|
||||
@@ -374,6 +472,140 @@ mod tests {
|
||||
assert_eq!(repl.new_content.msgtype.body(), "the edit");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_make_edit_caption_for_non_media_room_message() {
|
||||
let event_id = event_id!("$1");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.text_msg("hello world").event_id(event_id).sender(own_user_id).into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
|
||||
let err = make_edit_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
event_id,
|
||||
EditedContent::MediaCaption { caption: Some("yo".to_owned()), formatted_caption: None },
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_let!(EditError::IncompatibleEditType { target, new_content } = err);
|
||||
assert_eq!(target, "m.text");
|
||||
assert_eq!(new_content, "caption for a media room message");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_add_caption_for_media() {
|
||||
let event_id = event_id!("$1");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let filename = "rickroll.gif";
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
|
||||
.event_id(event_id)
|
||||
.sender(own_user_id)
|
||||
.into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
|
||||
let edit_event = make_edit_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
event_id,
|
||||
EditedContent::MediaCaption {
|
||||
caption: Some("Best joke ever".to_owned()),
|
||||
formatted_caption: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event);
|
||||
assert_let!(MessageType::Image(image) = msg.msgtype);
|
||||
|
||||
assert_eq!(image.filename(), filename);
|
||||
assert_eq!(image.caption(), Some("* Best joke ever")); // Fallback for a replacement 🤷
|
||||
assert!(image.formatted_caption().is_none());
|
||||
|
||||
assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to);
|
||||
assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype);
|
||||
assert_eq!(new_image.filename(), filename);
|
||||
assert_eq!(new_image.caption(), Some("Best joke ever"));
|
||||
assert!(new_image.formatted_caption().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_remove_caption_for_media() {
|
||||
let event_id = event_id!("$1");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let filename = "rickroll.gif";
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
|
||||
let event: SyncTimelineEvent = f
|
||||
.image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
|
||||
.caption(Some("caption".to_owned()), None)
|
||||
.event_id(event_id)
|
||||
.sender(own_user_id)
|
||||
.into();
|
||||
|
||||
{
|
||||
// Sanity checks.
|
||||
let event = event.raw().deserialize().unwrap();
|
||||
assert_let!(AnySyncTimelineEvent::MessageLike(event) = event);
|
||||
assert_let!(
|
||||
AnyMessageLikeEventContent::RoomMessage(msg) = event.original_content().unwrap()
|
||||
);
|
||||
assert_let!(MessageType::Image(image) = msg.msgtype);
|
||||
assert_eq!(image.filename(), filename);
|
||||
assert_eq!(image.caption(), Some("caption"));
|
||||
assert!(image.formatted_caption().is_none());
|
||||
}
|
||||
|
||||
cache.events.insert(event_id.to_owned(), event);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
|
||||
let edit_event = make_edit_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
event_id,
|
||||
// Remove the caption by setting it to None.
|
||||
EditedContent::MediaCaption { caption: None, formatted_caption: None },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event);
|
||||
assert_let!(MessageType::Image(image) = msg.msgtype);
|
||||
|
||||
assert_eq!(image.filename(), "* rickroll.gif"); // Fallback for a replacement 🤷
|
||||
assert!(image.caption().is_none());
|
||||
assert!(image.formatted_caption().is_none());
|
||||
|
||||
assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to);
|
||||
assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype);
|
||||
assert_eq!(new_image.filename(), "rickroll.gif");
|
||||
assert!(new_image.caption().is_none());
|
||||
assert!(new_image.formatted_caption().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_make_edit_event_success_with_response() {
|
||||
let event_id = event_id!("$1");
|
||||
|
||||
@@ -34,13 +34,16 @@ use ruma::{
|
||||
relation::{Annotation, InReplyTo, Replacement, Thread},
|
||||
room::{
|
||||
encrypted::{EncryptedEventScheme, RoomEncryptedEventContent},
|
||||
message::{Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation},
|
||||
message::{
|
||||
FormattedBody, ImageMessageEventContent, MessageType, Relation,
|
||||
RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
redaction::RoomRedactionEventContent,
|
||||
},
|
||||
AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, EventContent,
|
||||
},
|
||||
serde::Raw,
|
||||
server_name, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId,
|
||||
server_name, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId,
|
||||
OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
@@ -232,6 +235,37 @@ impl EventBuilder<RoomMessageEventContent> {
|
||||
Some(Relation::Replacement(Replacement::new(edited_event_id.to_owned(), new_content)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a caption to a media event.
|
||||
///
|
||||
/// Will crash if the event isn't a media room message.
|
||||
pub fn caption(
|
||||
mut self,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
) -> Self {
|
||||
match &mut self.content.msgtype {
|
||||
MessageType::Image(image) => {
|
||||
let filename = image.filename().to_owned();
|
||||
if let Some(caption) = caption {
|
||||
image.body = caption;
|
||||
image.filename = Some(filename);
|
||||
} else {
|
||||
image.body = filename;
|
||||
image.filename = None;
|
||||
}
|
||||
image.formatted = formatted_caption;
|
||||
}
|
||||
|
||||
MessageType::Audio(_) | MessageType::Video(_) | MessageType::File(_) => {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
_ => panic!("unexpected event type for a caption"),
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: EventContent> From<EventBuilder<E>> for Raw<AnySyncTimelineEvent>
|
||||
@@ -413,6 +447,17 @@ impl EventFactory {
|
||||
self.event(poll_end_content)
|
||||
}
|
||||
|
||||
/// Creates a plain (unencrypted) image event content referencing the given
|
||||
/// MXC ID.
|
||||
pub fn image(
|
||||
&self,
|
||||
filename: String,
|
||||
url: OwnedMxcUri,
|
||||
) -> EventBuilder<RoomMessageEventContent> {
|
||||
let image_event_content = ImageMessageEventContent::plain(filename, url);
|
||||
self.event(RoomMessageEventContent::new(MessageType::Image(image_event_content)))
|
||||
}
|
||||
|
||||
/// Set the next server timestamp.
|
||||
///
|
||||
/// Timestamps will continue to increase by 1 (millisecond) from that value.
|
||||
|
||||
Reference in New Issue
Block a user