diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs index 5b1b9421..cb346cfb 100644 --- a/crates/ruma-common/src/events/room/message.rs +++ b/crates/ruma-common/src/events/room/message.rs @@ -112,11 +112,9 @@ impl RoomMessageEventContent { reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { - let quoted = reply::get_plain_quote_fallback(original_message); - let quoted_html = reply::get_html_quote_fallback(original_message); - - let body = format!("{}\n\n{}", quoted, reply); - let html_body = format!("{}\n\n{}", quoted_html, reply); + 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 { @@ -137,11 +135,8 @@ impl RoomMessageEventContent { html_reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { - let quoted = reply::get_plain_quote_fallback(original_message); - let quoted_html = reply::get_html_quote_fallback(original_message); - - let body = format!("{}\n\n{}", quoted, reply); - let html_body = format!("{}\n\n{}", quoted_html, html_reply); + let (body, html_body) = + reply::plain_and_formatted_reply_body(reply, Some(html_reply), original_message); Self { relates_to: Some(Relation::Reply { @@ -161,11 +156,9 @@ impl RoomMessageEventContent { reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { - let quoted = reply::get_plain_quote_fallback(original_message); - let quoted_html = reply::get_html_quote_fallback(original_message); - - let body = format!("{}\n\n{}", quoted, reply); - let html_body = format!("{}\n\n{}", quoted_html, reply); + 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 { @@ -186,11 +179,8 @@ impl RoomMessageEventContent { html_reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { - let quoted = reply::get_plain_quote_fallback(original_message); - let quoted_html = reply::get_html_quote_fallback(original_message); - - let body = format!("{}\n\n{}", quoted, reply); - let html_body = format!("{}\n\n{}", quoted_html, html_reply); + let (body, html_body) = + reply::plain_and_formatted_reply_body(reply, Some(html_reply), original_message); Self { relates_to: Some(Relation::Reply { @@ -200,6 +190,106 @@ impl RoomMessageEventContent { } } + /// Create a new reply with the given message and optionally forwards the [`Relation::Thread`]. + /// + /// If `message` is a text or notice message, it is modified to include the rich reply fallback. + #[cfg(feature = "unstable-msc3440")] + pub fn reply( + message: MessageType, + original_message: &OriginalRoomMessageEvent, + forward_thread: ForwardThread, + ) -> Self { + let msgtype = match message { + MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { + let (body, html_body) = reply::plain_and_formatted_reply_body( + body, + formatted.map(|f| f.body), + original_message, + ); + + MessageType::Text(TextMessageEventContent::html(body, html_body)) + } + MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { + let (body, html_body) = reply::plain_and_formatted_reply_body( + body, + formatted.map(|f| f.body), + original_message, + ); + + MessageType::Notice(NoticeMessageEventContent::html(body, html_body)) + } + _ => message, + }; + + let relates_to = if let Some(Relation::Thread(Thread { event_id, .. })) = original_message + .content + .relates_to + .as_ref() + .filter(|_| forward_thread == ForwardThread::Yes) + { + Relation::Thread(Thread::reply(event_id.clone(), original_message.event_id.clone())) + } else { + Relation::Reply { + in_reply_to: InReplyTo { event_id: original_message.event_id.clone() }, + } + }; + + Self { msgtype, relates_to: Some(relates_to) } + } + + /// Create a new message for a thread that is optionally a reply. + /// + /// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, a message for the same + /// thread is created. If it doesn't, a new thread with `previous_message` as the root is + /// created. + /// + /// If `message` is a text or notice message, it is modified to include the rich reply fallback. + #[cfg(feature = "unstable-msc3440")] + pub fn for_thread( + message: MessageType, + previous_message: &OriginalRoomMessageEvent, + is_reply: ReplyInThread, + ) -> Self { + let msgtype = match message { + MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { + let (body, html_body) = reply::plain_and_formatted_reply_body( + body, + formatted.map(|f| f.body), + previous_message, + ); + + MessageType::Text(TextMessageEventContent::html(body, html_body)) + } + MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { + let (body, html_body) = reply::plain_and_formatted_reply_body( + body, + formatted.map(|f| f.body), + previous_message, + ); + + MessageType::Notice(NoticeMessageEventContent::html(body, html_body)) + } + _ => message, + }; + + let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) = + &previous_message.content.relates_to + { + event_id.clone() + } else { + previous_message.event_id.clone() + }; + + Self { + msgtype, + relates_to: Some(Relation::Thread(Thread { + event_id: thread_root, + in_reply_to: InReplyTo { event_id: previous_message.event_id.clone() }, + is_falling_back: is_reply == ReplyInThread::No, + })), + } + } + /// Returns a reference to the `msgtype` string. /// /// If you want to access the message type-specific data rather than the message type itself, @@ -325,6 +415,42 @@ impl From for RoomMessageEventContent { } } +/// Whether or not to forward a [`Relation::Thread`] when sending a reply. +#[cfg(feature = "unstable-msc3440")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[allow(clippy::exhaustive_enums)] +pub enum ForwardThread { + /// The thread relation in the original message is forwarded if it exists. + /// + /// This should be set if your client doesn't support threads (see [MSC3440]). + /// + /// [MSC3440]: https://github.com/matrix-org/matrix-spec-proposals/pull/3440 + Yes, + + /// Create a reply in the main conversation even if the original message is in a thread. + /// + /// This should be used if you client supports threads and you explicitly want that behavior. + No, +} + +/// Whether or not the message is a reply inside a thread. +#[cfg(feature = "unstable-msc3440")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[allow(clippy::exhaustive_enums)] +pub enum ReplyInThread { + /// This is a reply. + /// + /// Create a proper reply _in_ the thread. + Yes, + + /// This is not a reply. + /// + /// Create a regular message in the thread, with a reply fallback, according to [MSC3440]. + /// + /// [MSC3440]: https://github.com/matrix-org/matrix-spec-proposals/pull/3440 + No, +} + /// The content that is specific to each message type variant. #[derive(Clone, Debug, Serialize)] #[serde(untagged)] @@ -566,13 +692,13 @@ impl Thread { /// Convenience method to create a regular `Thread` with the given event ID and latest /// message-like event ID. pub fn plain(event_id: Box, latest_event_id: Box) -> Self { - Self { event_id, in_reply_to: InReplyTo::new(latest_event_id), is_falling_back: false } + Self { event_id, in_reply_to: InReplyTo::new(latest_event_id), is_falling_back: true } } /// Convenience method to create a reply `Thread` with the given event ID and replied-to event /// ID. pub fn reply(event_id: Box, reply_to_event_id: Box) -> Self { - Self { event_id, in_reply_to: InReplyTo::new(reply_to_event_id), is_falling_back: true } + Self { event_id, in_reply_to: InReplyTo::new(reply_to_event_id), is_falling_back: false } } } diff --git a/crates/ruma-common/src/events/room/message/reply.rs b/crates/ruma-common/src/events/room/message/reply.rs index 84f41885..71972845 100644 --- a/crates/ruma-common/src/events/room/message/reply.rs +++ b/crates/ruma-common/src/events/room/message/reply.rs @@ -1,3 +1,5 @@ +use std::fmt; + use indoc::formatdoc; use super::{FormattedBody, MessageType, OriginalRoomMessageEvent}; @@ -250,6 +252,27 @@ fn formatted_or_plain_body<'a>(formatted: &'a Option, body: &'a s } } +/// Get the plain and formatted body for a rich reply. +/// +/// Returns a `(plain, html)` tuple. +pub fn plain_and_formatted_reply_body( + body: impl fmt::Display, + formatted: Option, + original_message: &OriginalRoomMessageEvent, +) -> (String, String) { + let quoted = get_plain_quote_fallback(original_message); + let quoted_html = get_html_quote_fallback(original_message); + + let plain = format!("{}\n\n{}", quoted, body); + let html = if let Some(formatted) = formatted { + format!("{}\n\n{}", quoted_html, formatted) + } else { + format!("{}\n\n{}", quoted_html, body) + }; + + (plain, html) +} + #[cfg(test)] mod tests { use crate::{ diff --git a/crates/ruma-common/tests/events/relations.rs b/crates/ruma-common/tests/events/relations.rs index 96769ff3..73864c8a 100644 --- a/crates/ruma-common/tests/events/relations.rs +++ b/crates/ruma-common/tests/events/relations.rs @@ -178,6 +178,7 @@ fn thread_plain_serialize() { "m.in_reply_to": { "event_id": "$latesteventid", }, + "io.element.show_reply": true, }, }) ); @@ -195,6 +196,7 @@ fn thread_plain_serialize() { "m.in_reply_to": { "event_id": "$latesteventid", }, + "io.element.show_reply": true, }, }) ); @@ -229,7 +231,6 @@ fn thread_reply_serialize() { "m.in_reply_to": { "event_id": "$repliedtoeventid", }, - "io.element.show_reply": true, }, }) ); @@ -247,7 +248,6 @@ fn thread_reply_serialize() { "m.in_reply_to": { "event_id": "$repliedtoeventid", }, - "io.element.show_reply": true, }, }) );