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 --> <!-- 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 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 [`OriginalSyncRoomMessageEvent`], you have to convert it first by calling
[`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event]. [`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event].

View File

@ -103,76 +103,116 @@ impl RoomMessageEventContent {
Self::new(MessageType::Notice(NoticeMessageEventContent::markdown(body))) 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. /// Creates a plain text reply to a message.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[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( pub fn text_reply_plain(
reply: impl fmt::Display, reply: impl fmt::Display,
original_message: &OriginalRoomMessageEvent, original_message: &OriginalRoomMessageEvent,
) -> Self { ) -> Self {
let formatted: Option<&str> = None; Self::text_plain(reply.to_string()).make_reply_to(original_message)
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)
}
} }
/// Creates a html text reply to a message. /// Creates a html text reply to a message.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[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( pub fn text_reply_html(
reply: impl fmt::Display, reply: impl fmt::Display,
html_reply: impl fmt::Display, html_reply: impl fmt::Display,
original_message: &OriginalRoomMessageEvent, original_message: &OriginalRoomMessageEvent,
) -> Self { ) -> Self {
let (body, html_body) = Self::text_html(reply.to_string(), html_reply.to_string()).make_reply_to(original_message)
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)
}
} }
/// Creates a plain text notice reply to a message. /// Creates a plain text notice reply to a message.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[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( pub fn notice_reply_plain(
reply: impl fmt::Display, reply: impl fmt::Display,
original_message: &OriginalRoomMessageEvent, original_message: &OriginalRoomMessageEvent,
) -> Self { ) -> Self {
let formatted: Option<&str> = None; Self::notice_plain(reply.to_string()).make_reply_to(original_message)
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)
}
} }
/// Creates a html text notice reply to a message. /// Creates a html text notice reply to a message.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] #[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( pub fn notice_reply_html(
reply: impl fmt::Display, reply: impl fmt::Display,
html_reply: impl fmt::Display, html_reply: impl fmt::Display,
original_message: &OriginalRoomMessageEvent, original_message: &OriginalRoomMessageEvent,
) -> Self { ) -> Self {
let (body, html_body) = Self::notice_html(reply.to_string(), html_reply.to_string()).make_reply_to(original_message)
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)
}
} }
/// Create a new reply with the given message and optionally forwards the [`Relation::Thread`]. /// Create a new reply with the given message and optionally forwards the [`Relation::Thread`].

View File

@ -8,37 +8,7 @@ use super::{
use super::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback}; use super::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback};
fn get_message_quote_fallbacks(original_message: &OriginalRoomMessageEvent) -> (String, String) { fn get_message_quote_fallbacks(original_message: &OriginalRoomMessageEvent) -> (String, String) {
match &original_message.content.msgtype { let get_quotes = |body: &str, formatted: Option<&FormattedBody>, is_emote: bool| {
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)
}
}
}
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 OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = original_message;
let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. })); let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. }));
let emote_sign = is_emote.then(|| "* ").unwrap_or_default(); let emote_sign = is_emote.then(|| "* ").unwrap_or_default();
@ -61,6 +31,21 @@ fn get_quotes(
</mx-reply>" </mx-reply>"
), ),
) )
};
match &original_message.content.msgtype {
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 formatted_or_plain_body( fn formatted_or_plain_body(
@ -119,10 +104,9 @@ pub fn plain_and_formatted_reply_body(
let (quoted, quoted_html) = get_message_quote_fallbacks(original_message); let (quoted, quoted_html) = get_message_quote_fallbacks(original_message);
let plain = format!("{quoted}\n{body}"); let plain = format!("{quoted}\n{body}");
let html = if let Some(formatted) = formatted { let html = match formatted {
format!("{quoted_html}{formatted}") Some(formatted) => format!("{quoted_html}{formatted}"),
} else { None => format!("{quoted_html}{body}"),
format!("{quoted_html}{body}")
}; };
(plain, html) (plain, html)
@ -160,7 +144,7 @@ mod tests {
<br>\ <br>\
multi<br>line\ multi<br>line\
</blockquote>\ </blockquote>\
</mx-reply>" </mx-reply>",
); );
} }
} }

View File

@ -500,22 +500,22 @@ fn reply_sanitize() {
unsigned: MessageLikeUnsigned::default(), unsigned: MessageLikeUnsigned::default(),
}; };
let second_message = OriginalRoomMessageEvent { let second_message = OriginalRoomMessageEvent {
content: RoomMessageEventContent::text_reply_html( content: RoomMessageEventContent::text_html(
"This is the _second_ message", "This is the _second_ message",
"This is the <em>second</em> message", "This is the <em>second</em> message",
&first_message, )
), .make_reply_to(&first_message),
event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
room_id: room_id!("!testroomid:example.org").to_owned(), room_id: room_id!("!testroomid:example.org").to_owned(),
sender: user_id!("@user:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(),
unsigned: MessageLikeUnsigned::default(), unsigned: MessageLikeUnsigned::default(),
}; };
let final_reply = RoomMessageEventContent::text_reply_html( let final_reply = RoomMessageEventContent::text_html(
"This is **my** reply", "This is **my** reply",
"This is <strong>my</strong> reply", "This is <strong>my</strong> reply",
&second_message, )
); .make_reply_to(&second_message);
let (body, formatted) = assert_matches!( let (body, formatted) = assert_matches!(
first_message.content.msgtype, first_message.content.msgtype,