events: Add RoomMessageEventContent::make_reply_to

… and deprecate reply constructors.
This commit is contained in:
Jonas Platte 2022-09-14 00:01:16 +02:00
parent 5c3610b9b7
commit b7b7d043f3
No known key found for this signature in database
GPG Key ID: 7D261D771D915378
4 changed files with 125 additions and 101 deletions

View File

@ -1,5 +1,5 @@
<!-- Keep this comment so the content is always included as a new paragraph -->
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].

View File

@ -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`].

View File

@ -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!(
"<mx-reply>\
<blockquote>\
<a href=\"https://matrix.to/#/{room_id}/{event_id}\">In reply to</a> \
{emote_sign}<a href=\"https://matrix.to/#/{sender}\">{sender}</a>\
<br>\
{html_body}\
</blockquote>\
</mx-reply>"
),
)
};
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!(
"<mx-reply>\
<blockquote>\
<a href=\"https://matrix.to/#/{room_id}/{event_id}\">In reply to</a> \
{emote_sign}<a href=\"https://matrix.to/#/{sender}\">{sender}</a>\
<br>\
{html_body}\
</blockquote>\
</mx-reply>"
),
)
}
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 {
<br>\
multi<br>line\
</blockquote>\
</mx-reply>"
</mx-reply>",
);
}
}

View File

@ -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 <em>second</em> 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 <strong>my</strong> reply",
&second_message,
);
)
.make_reply_to(&second_message);
let (body, formatted) = assert_matches!(
first_message.content.msgtype,