diff --git a/crates/ruma-common/src/doc/rich_reply.md b/crates/ruma-common/src/doc/rich_reply.md
index a47467ad..1097e0c9 100644
--- a/crates/ruma-common/src/doc/rich_reply.md
+++ b/crates/ruma-common/src/doc/rich_reply.md
@@ -1,5 +1,5 @@
-This constructor requires an [`OriginalRoomMessageEvent`] since it creates a permalink to
+This function requires an [`OriginalRoomMessageEvent`] since it creates a permalink to
the previous message, for which the room ID is required. If you want to reply to an
[`OriginalSyncRoomMessageEvent`], you have to convert it first by calling
[`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event].
diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs
index 0937a314..800c96b3 100644
--- a/crates/ruma-common/src/events/room/message.rs
+++ b/crates/ruma-common/src/events/room/message.rs
@@ -103,76 +103,116 @@ impl RoomMessageEventContent {
Self::new(MessageType::Notice(NoticeMessageEventContent::markdown(body)))
}
+ /// Turns `self` into a reply to the given message.
+ ///
+ /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
+ /// quoted version of `original_message`. Also sets the `in_reply_to` field inside `relates_to`.
+ #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
+ ///
+ /// # Panics
+ ///
+ /// Panics if `self` has a `formatted_body` with a format other than HTML.
+ #[track_caller]
+ pub fn make_reply_to(mut self, original_message: &OriginalRoomMessageEvent) -> Self {
+ let empty_formatted_body = || FormattedBody::html(String::new());
+
+ let (body, formatted) = {
+ match &mut self.msgtype {
+ MessageType::Emote(m) => {
+ (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
+ }
+ MessageType::Notice(m) => {
+ (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
+ }
+ MessageType::Text(m) => {
+ (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
+ }
+ MessageType::Audio(m) => (&mut m.body, None),
+ MessageType::File(m) => (&mut m.body, None),
+ MessageType::Image(m) => (&mut m.body, None),
+ MessageType::Location(m) => (&mut m.body, None),
+ MessageType::ServerNotice(m) => (&mut m.body, None),
+ MessageType::Video(m) => (&mut m.body, None),
+ MessageType::VerificationRequest(m) => (&mut m.body, None),
+ MessageType::_Custom(m) => (&mut m.body, None),
+ }
+ };
+
+ if let Some(f) = formatted {
+ assert_eq!(
+ f.format,
+ MessageFormat::Html,
+ "make_reply_to can't handle non-HTML formatted messages"
+ );
+
+ let formatted_body = &mut f.body;
+
+ (*body, *formatted_body) = reply::plain_and_formatted_reply_body(
+ body.as_str(),
+ (!formatted_body.is_empty()).then(|| formatted_body.as_str()),
+ original_message,
+ );
+ }
+
+ self.relates_to = Some(Relation::Reply {
+ in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() },
+ });
+
+ self
+ }
+
/// Creates a plain text reply to a message.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
+ #[deprecated = "\
+ use [`Self::text_plain`](#method.text_plain)`(reply).`\
+ [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\
+ "]
pub fn text_reply_plain(
reply: impl fmt::Display,
original_message: &OriginalRoomMessageEvent,
) -> Self {
- let formatted: Option<&str> = None;
- let (body, html_body) =
- reply::plain_and_formatted_reply_body(reply, formatted, original_message);
-
- Self {
- relates_to: Some(Relation::Reply {
- in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() },
- }),
- ..Self::text_html(body, html_body)
- }
+ Self::text_plain(reply.to_string()).make_reply_to(original_message)
}
/// Creates a html text reply to a message.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
+ #[deprecated = "\
+ use [`Self::text_html`](#method.text_html)`(reply, html_reply).`\
+ [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\
+ "]
pub fn text_reply_html(
reply: impl fmt::Display,
html_reply: impl fmt::Display,
original_message: &OriginalRoomMessageEvent,
) -> Self {
- let (body, html_body) =
- reply::plain_and_formatted_reply_body(reply, Some(html_reply), original_message);
-
- Self {
- relates_to: Some(Relation::Reply {
- in_reply_to: InReplyTo { event_id: original_message.event_id.clone() },
- }),
- ..Self::text_html(body, html_body)
- }
+ Self::text_html(reply.to_string(), html_reply.to_string()).make_reply_to(original_message)
}
/// Creates a plain text notice reply to a message.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
+ #[deprecated = "\
+ use [`Self::notice_plain`](#method.notice_plain)`(reply).`\
+ [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\
+ "]
pub fn notice_reply_plain(
reply: impl fmt::Display,
original_message: &OriginalRoomMessageEvent,
) -> Self {
- let formatted: Option<&str> = None;
- let (body, html_body) =
- reply::plain_and_formatted_reply_body(reply, formatted, original_message);
-
- Self {
- relates_to: Some(Relation::Reply {
- in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() },
- }),
- ..Self::notice_html(body, html_body)
- }
+ Self::notice_plain(reply.to_string()).make_reply_to(original_message)
}
/// Creates a html text notice reply to a message.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
+ #[deprecated = "\
+ use [`Self::notice_html`](#method.notice_html)`(reply, html_reply).`\
+ [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\
+ "]
pub fn notice_reply_html(
reply: impl fmt::Display,
html_reply: impl fmt::Display,
original_message: &OriginalRoomMessageEvent,
) -> Self {
- let (body, html_body) =
- reply::plain_and_formatted_reply_body(reply, Some(html_reply), original_message);
-
- Self {
- relates_to: Some(Relation::Reply {
- in_reply_to: InReplyTo { event_id: original_message.event_id.clone() },
- }),
- ..Self::notice_html(body, html_body)
- }
+ Self::notice_html(reply.to_string(), html_reply.to_string()).make_reply_to(original_message)
}
/// Create a new reply with the given message and optionally forwards the [`Relation::Thread`].
diff --git a/crates/ruma-common/src/events/room/message/reply.rs b/crates/ruma-common/src/events/room/message/reply.rs
index 17832eb1..6cbb6554 100644
--- a/crates/ruma-common/src/events/room/message/reply.rs
+++ b/crates/ruma-common/src/events/room/message/reply.rs
@@ -8,61 +8,46 @@ use super::{
use super::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback};
fn get_message_quote_fallbacks(original_message: &OriginalRoomMessageEvent) -> (String, String) {
+ let get_quotes = |body: &str, formatted: Option<&FormattedBody>, is_emote: bool| {
+ let OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = original_message;
+ let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. }));
+ let emote_sign = is_emote.then(|| "* ").unwrap_or_default();
+ let body = is_reply.then(|| remove_plain_reply_fallback(body)).unwrap_or(body);
+ #[cfg(feature = "unstable-sanitize")]
+ let html_body = formatted_or_plain_body(formatted, body, is_reply);
+ #[cfg(not(feature = "unstable-sanitize"))]
+ let html_body = formatted_or_plain_body(formatted, body);
+
+ (
+ format!("> {emote_sign}<{sender}> {body}").replace('\n', "\n> "),
+ format!(
+ "\
+ \
+ In reply to \
+ {emote_sign}{sender}\
+
\
+ {html_body}\
+
\
+ "
+ ),
+ )
+ };
+
match &original_message.content.msgtype {
- MessageType::Audio(_) => get_quotes("sent an audio file.", None, original_message, false),
- MessageType::Emote(content) => {
- get_quotes(&content.body, content.formatted.as_ref(), original_message, true)
- }
- MessageType::File(_) => get_quotes("sent a file.", None, original_message, false),
- MessageType::Image(_) => get_quotes("sent an image.", None, original_message, false),
- MessageType::Location(_) => get_quotes("sent a location.", None, original_message, false),
- MessageType::Notice(content) => {
- get_quotes(&content.body, content.formatted.as_ref(), original_message, false)
- }
- MessageType::ServerNotice(content) => {
- get_quotes(&content.body, None, original_message, false)
- }
- MessageType::Text(content) => {
- get_quotes(&content.body, content.formatted.as_ref(), original_message, false)
- }
- MessageType::Video(_) => get_quotes("sent a video.", None, original_message, false),
- MessageType::_Custom(content) => get_quotes(&content.body, None, original_message, false),
- MessageType::VerificationRequest(content) => {
- get_quotes(&content.body, None, original_message, false)
- }
+ MessageType::Audio(_) => get_quotes("sent an audio file.", None, false),
+ MessageType::Emote(c) => get_quotes(&c.body, c.formatted.as_ref(), true),
+ MessageType::File(_) => get_quotes("sent a file.", None, false),
+ MessageType::Image(_) => get_quotes("sent an image.", None, false),
+ MessageType::Location(_) => get_quotes("sent a location.", None, false),
+ MessageType::Notice(c) => get_quotes(&c.body, c.formatted.as_ref(), false),
+ MessageType::ServerNotice(c) => get_quotes(&c.body, None, false),
+ MessageType::Text(c) => get_quotes(&c.body, c.formatted.as_ref(), false),
+ MessageType::Video(_) => get_quotes("sent a video.", None, false),
+ MessageType::VerificationRequest(content) => get_quotes(&content.body, None, false),
+ MessageType::_Custom(content) => get_quotes(&content.body, None, false),
}
}
-fn get_quotes(
- body: &str,
- formatted: Option<&FormattedBody>,
- original_message: &OriginalRoomMessageEvent,
- is_emote: bool,
-) -> (String, String) {
- let OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = original_message;
- let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. }));
- let emote_sign = is_emote.then(|| "* ").unwrap_or_default();
- let body = is_reply.then(|| remove_plain_reply_fallback(body)).unwrap_or(body);
- #[cfg(feature = "unstable-sanitize")]
- let html_body = formatted_or_plain_body(formatted, body, is_reply);
- #[cfg(not(feature = "unstable-sanitize"))]
- let html_body = formatted_or_plain_body(formatted, body);
-
- (
- format!("> {emote_sign}<{sender}> {body}").replace('\n', "\n> "),
- format!(
- "\
- \
- In reply to \
- {emote_sign}{sender}\
-
\
- {html_body}\
-
\
- "
- ),
- )
-}
-
fn formatted_or_plain_body(
formatted: Option<&FormattedBody>,
body: &str,
@@ -119,10 +104,9 @@ pub fn plain_and_formatted_reply_body(
let (quoted, quoted_html) = get_message_quote_fallbacks(original_message);
let plain = format!("{quoted}\n{body}");
- let html = if let Some(formatted) = formatted {
- format!("{quoted_html}{formatted}")
- } else {
- format!("{quoted_html}{body}")
+ let html = match formatted {
+ Some(formatted) => format!("{quoted_html}{formatted}"),
+ None => format!("{quoted_html}{body}"),
};
(plain, html)
@@ -160,7 +144,7 @@ mod tests {
\
multi
line\
\
- "
+ ",
);
}
}
diff --git a/crates/ruma-common/tests/events/room_message.rs b/crates/ruma-common/tests/events/room_message.rs
index c1364cae..506ab87e 100644
--- a/crates/ruma-common/tests/events/room_message.rs
+++ b/crates/ruma-common/tests/events/room_message.rs
@@ -500,22 +500,22 @@ fn reply_sanitize() {
unsigned: MessageLikeUnsigned::default(),
};
let second_message = OriginalRoomMessageEvent {
- content: RoomMessageEventContent::text_reply_html(
+ content: RoomMessageEventContent::text_html(
"This is the _second_ message",
"This is the second message",
- &first_message,
- ),
+ )
+ .make_reply_to(&first_message),
event_id: event_id!("$143273582443PhrSn:example.org").to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
room_id: room_id!("!testroomid:example.org").to_owned(),
sender: user_id!("@user:example.org").to_owned(),
unsigned: MessageLikeUnsigned::default(),
};
- let final_reply = RoomMessageEventContent::text_reply_html(
+ let final_reply = RoomMessageEventContent::text_html(
"This is **my** reply",
"This is my reply",
- &second_message,
- );
+ )
+ .make_reply_to(&second_message);
let (body, formatted) = assert_matches!(
first_message.content.msgtype,