events: Do not generate reply fallbacks anymore
According to MSC2781. The helper methods for the relevant relations have been simplified to only take the necessary data.
This commit is contained in:
		
							parent
							
								
									ea0cdc6318
								
							
						
					
					
						commit
						3321286242
					
				@ -1,5 +1,26 @@
 | 
			
		||||
# [unreleased]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
- Reply fallbacks are not generated anymore, according to MSC2781 / Matrix 1.13.
 | 
			
		||||
  As a result, the following methods of `RoomMessageEventContent(WithoutRelation)`
 | 
			
		||||
  were simplified:
 | 
			
		||||
  - `make_reply_to` and `make_reply_to_raw` have been merged into
 | 
			
		||||
    `make_reply_to`. It takes a `ReplyMetadata`, that can be virtually
 | 
			
		||||
    constructed from any event and includes built-in conversions for room
 | 
			
		||||
    message events.
 | 
			
		||||
  - `make_for_thread` also takes a `ReplyMetadata` instead of a room message
 | 
			
		||||
    event.
 | 
			
		||||
  - `make_replacement` does not take the replied-to message anymore.
 | 
			
		||||
 | 
			
		||||
# 0.30.0
 | 
			
		||||
 | 
			
		||||
Breaking changes:
 | 
			
		||||
 | 
			
		||||
- Take newly introduced `DirectUserIdentifier(str)` as a key for `DirectEventContent`.
 | 
			
		||||
  This change allows to have an email or MSISDN phone number as a key for example,
 | 
			
		||||
  which can be used when issuing invites through third-party systems.
 | 
			
		||||
  `DirectUserIdentifier` can easily be converted to an `UserId`.
 | 
			
		||||
 | 
			
		||||
Improvements:
 | 
			
		||||
 | 
			
		||||
- Add unstable support for MSC4171 for the m.member_hints state event.
 | 
			
		||||
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
<!-- Keep this comment so the content is always included as a new paragraph -->
 | 
			
		||||
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()`].
 | 
			
		||||
 | 
			
		||||
If the message was edited, the previous message should be the original message that was edited,
 | 
			
		||||
with the content of its replacement, to allow the fallback to be accurate at the time it is added.
 | 
			
		||||
 | 
			
		||||
It is recommended to enable the `html` feature when using this method as this will
 | 
			
		||||
clean up nested [rich reply fallbacks] in chains of replies. This uses [`sanitize_html()`]
 | 
			
		||||
internally, with [`RemoveReplyFallback::Yes`].
 | 
			
		||||
 | 
			
		||||
[`OriginalRoomMessageEvent`]: crate::room::message::OriginalRoomMessageEvent
 | 
			
		||||
[`OriginalSyncRoomMessageEvent`]: crate::room::message::OriginalSyncRoomMessageEvent
 | 
			
		||||
[`.into_full_event()`]: crate::OriginalSyncMessageLikeEvent::into_full_event
 | 
			
		||||
[rich reply fallbacks]: https://spec.matrix.org/latest/client-server-api/#fallbacks-for-rich-replies
 | 
			
		||||
[`sanitize_html()`]: ruma_html::sanitize_html
 | 
			
		||||
[`RemoveReplyFallback::Yes`]: ruma_html::RemoveReplyFallback::Yes
 | 
			
		||||
@ -4,9 +4,10 @@
 | 
			
		||||
 | 
			
		||||
use std::borrow::Cow;
 | 
			
		||||
 | 
			
		||||
use as_variant::as_variant;
 | 
			
		||||
use ruma_common::{
 | 
			
		||||
    serde::{JsonObject, Raw, StringEnum},
 | 
			
		||||
    OwnedEventId, RoomId,
 | 
			
		||||
    serde::{JsonObject, StringEnum},
 | 
			
		||||
    EventId, OwnedEventId, UserId,
 | 
			
		||||
};
 | 
			
		||||
#[cfg(feature = "html")]
 | 
			
		||||
use ruma_html::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback};
 | 
			
		||||
@ -15,12 +16,11 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
 | 
			
		||||
use serde_json::Value as JsonValue;
 | 
			
		||||
use tracing::warn;
 | 
			
		||||
 | 
			
		||||
use self::reply::OriginalEventData;
 | 
			
		||||
#[cfg(feature = "html")]
 | 
			
		||||
use self::sanitize::remove_plain_reply_fallback;
 | 
			
		||||
use crate::{
 | 
			
		||||
    relation::{InReplyTo, Replacement, Thread},
 | 
			
		||||
    AnySyncTimelineEvent, Mentions, PrivOwnedStr,
 | 
			
		||||
    Mentions, PrivOwnedStr,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
mod audio;
 | 
			
		||||
@ -34,7 +34,6 @@ mod media_caption;
 | 
			
		||||
mod notice;
 | 
			
		||||
mod relation;
 | 
			
		||||
pub(crate) mod relation_serde;
 | 
			
		||||
mod reply;
 | 
			
		||||
pub mod sanitize;
 | 
			
		||||
mod server_notice;
 | 
			
		||||
mod text;
 | 
			
		||||
@ -151,84 +150,45 @@ impl RoomMessageEventContent {
 | 
			
		||||
        Self::new(MessageType::emote_markdown(body))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Turns `self` into a reply to the given message.
 | 
			
		||||
    /// Turns `self` into a [rich reply] to the message using the given metadata.
 | 
			
		||||
    ///
 | 
			
		||||
    /// 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`,
 | 
			
		||||
    /// and optionally the `rel_type` to `m.thread` if the `original_message is in a thread and
 | 
			
		||||
    /// thread forwarding is enabled.
 | 
			
		||||
    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
 | 
			
		||||
    /// Sets the `in_reply_to` field inside `relates_to`, and optionally the `rel_type` to
 | 
			
		||||
    /// `m.thread` if the metadata has a `thread` and `ForwardThread::Yes` is used.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Panics
 | 
			
		||||
    /// If `AddMentions::Yes` is used, the `sender` in the metadata is added as a user mention.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Panics if `self` has a `formatted_body` with a format other than HTML.
 | 
			
		||||
    /// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies
 | 
			
		||||
    #[track_caller]
 | 
			
		||||
    pub fn make_reply_to(
 | 
			
		||||
    pub fn make_reply_to<'a>(
 | 
			
		||||
        self,
 | 
			
		||||
        original_message: &OriginalRoomMessageEvent,
 | 
			
		||||
        metadata: impl Into<ReplyMetadata<'a>>,
 | 
			
		||||
        forward_thread: ForwardThread,
 | 
			
		||||
        add_mentions: AddMentions,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        self.without_relation().make_reply_to(original_message, forward_thread, add_mentions)
 | 
			
		||||
        self.without_relation().make_reply_to(metadata, forward_thread, add_mentions)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Turns `self` into a reply to the given raw event.
 | 
			
		||||
    /// Turns `self` into a new message for a [thread], that is optionally a reply.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
 | 
			
		||||
    /// quoted version of the `body` of `original_event` (if any). Also sets the `in_reply_to` field
 | 
			
		||||
    /// inside `relates_to`, and optionally the `rel_type` to `m.thread` if the
 | 
			
		||||
    /// `original_message is in a thread and thread forwarding is enabled.
 | 
			
		||||
    /// Looks for the `thread` in the given metadata. If it exists, this message will be in the same
 | 
			
		||||
    /// thread. If it doesn't, a new thread is created with the `event_id` in the metadata as the
 | 
			
		||||
    /// root.
 | 
			
		||||
    ///
 | 
			
		||||
    /// It is recommended to use [`Self::make_reply_to()`] for replies to `m.room.message` events,
 | 
			
		||||
    /// as the generated fallback is better for some `msgtype`s.
 | 
			
		||||
    /// It also sets the `in_reply_to` field inside `relates_to` to point the `event_id`
 | 
			
		||||
    /// in the metadata. If `ReplyWithinThread::Yes` is used, the metadata should be constructed
 | 
			
		||||
    /// from the event to make a reply to, otherwise it should be constructed from the latest
 | 
			
		||||
    /// event in the thread.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Note that except for the panic below, this is infallible. Which means that if a field is
 | 
			
		||||
    /// missing when deserializing the data, the changes that require it will not be applied. It
 | 
			
		||||
    /// will still at least apply the `m.in_reply_to` relation to this content.
 | 
			
		||||
    /// If `AddMentions::Yes` is used, the `sender` in the metadata is added as a user mention.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Panics
 | 
			
		||||
    ///
 | 
			
		||||
    /// Panics if `self` has a `formatted_body` with a format other than HTML.
 | 
			
		||||
    #[track_caller]
 | 
			
		||||
    pub fn make_reply_to_raw(
 | 
			
		||||
    /// [thread]: https://spec.matrix.org/latest/client-server-api/#threading
 | 
			
		||||
    pub fn make_for_thread<'a>(
 | 
			
		||||
        self,
 | 
			
		||||
        original_event: &Raw<AnySyncTimelineEvent>,
 | 
			
		||||
        original_event_id: OwnedEventId,
 | 
			
		||||
        room_id: &RoomId,
 | 
			
		||||
        forward_thread: ForwardThread,
 | 
			
		||||
        add_mentions: AddMentions,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        self.without_relation().make_reply_to_raw(
 | 
			
		||||
            original_event,
 | 
			
		||||
            original_event_id,
 | 
			
		||||
            room_id,
 | 
			
		||||
            forward_thread,
 | 
			
		||||
            add_mentions,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Turns `self` into a new message for a thread, that is optionally a reply.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, this message will be
 | 
			
		||||
    /// in the same thread. If it doesn't, a new thread with `previous_message` as the root is
 | 
			
		||||
    /// created.
 | 
			
		||||
    ///
 | 
			
		||||
    /// If this is a reply within the thread, takes the `body` / `formatted_body` (if any) in `self`
 | 
			
		||||
    /// for the main text and prepends a quoted version of `previous_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 this is a reply within the thread and `self` has a `formatted_body` with a format
 | 
			
		||||
    /// other than HTML.
 | 
			
		||||
    pub fn make_for_thread(
 | 
			
		||||
        self,
 | 
			
		||||
        previous_message: &OriginalRoomMessageEvent,
 | 
			
		||||
        metadata: impl Into<ReplyMetadata<'a>>,
 | 
			
		||||
        is_reply: ReplyWithinThread,
 | 
			
		||||
        add_mentions: AddMentions,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        self.without_relation().make_for_thread(previous_message, is_reply, add_mentions)
 | 
			
		||||
        self.without_relation().make_for_thread(metadata, is_reply, add_mentions)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Turns `self` into a [replacement] (or edit) for a given message.
 | 
			
		||||
@ -240,12 +200,6 @@ impl RoomMessageEventContent {
 | 
			
		||||
    /// This takes the content and sets it in `m.new_content`, and modifies the `content` to include
 | 
			
		||||
    /// a fallback.
 | 
			
		||||
    ///
 | 
			
		||||
    /// If the message that is replaced is a reply to another message, the latter should also be
 | 
			
		||||
    /// provided to be able to generate a rich reply fallback that takes the `body` /
 | 
			
		||||
    /// `formatted_body` (if any) in `self` for the main text and prepends a quoted version of
 | 
			
		||||
    /// `original_message`.
 | 
			
		||||
    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
 | 
			
		||||
    ///
 | 
			
		||||
    /// If this message contains [`Mentions`], they are copied into `m.new_content` to keep the same
 | 
			
		||||
    /// mentions, but the ones in `content` are filtered with the ones in the
 | 
			
		||||
    /// [`ReplacementMetadata`] so only new mentions will trigger a notification.
 | 
			
		||||
@ -256,12 +210,8 @@ impl RoomMessageEventContent {
 | 
			
		||||
    ///
 | 
			
		||||
    /// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
 | 
			
		||||
    #[track_caller]
 | 
			
		||||
    pub fn make_replacement(
 | 
			
		||||
        self,
 | 
			
		||||
        metadata: impl Into<ReplacementMetadata>,
 | 
			
		||||
        replied_to_message: Option<&OriginalRoomMessageEvent>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        self.without_relation().make_replacement(metadata, replied_to_message)
 | 
			
		||||
    pub fn make_replacement(self, metadata: impl Into<ReplacementMetadata>) -> Self {
 | 
			
		||||
        self.without_relation().make_replacement(metadata)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set the [mentions] of this event.
 | 
			
		||||
@ -375,6 +325,11 @@ impl RoomMessageEventContent {
 | 
			
		||||
 | 
			
		||||
        self.into()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the thread relation from this content, if any.
 | 
			
		||||
    fn thread(&self) -> Option<&Thread> {
 | 
			
		||||
        self.relates_to.as_ref().and_then(|relates_to| as_variant!(relates_to, Relation::Thread))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Whether or not to forward a [`Relation::Thread`] when sending a reply.
 | 
			
		||||
@ -661,49 +616,6 @@ impl MessageType {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[track_caller]
 | 
			
		||||
    fn add_reply_fallback(&mut self, original_event: OriginalEventData<'_>) {
 | 
			
		||||
        let empty_formatted_body = || FormattedBody::html(String::new());
 | 
			
		||||
 | 
			
		||||
        let (body, formatted) = {
 | 
			
		||||
            match self {
 | 
			
		||||
                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,
 | 
			
		||||
                "can't add reply fallback to 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_some(formatted_body.as_str()),
 | 
			
		||||
                original_event,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn make_replacement_body(&mut self) {
 | 
			
		||||
        let empty_formatted_body = || FormattedBody::html(String::new());
 | 
			
		||||
 | 
			
		||||
@ -784,6 +696,39 @@ impl From<&OriginalSyncRoomMessageEvent> for ReplacementMetadata {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Metadata about an event to reply to or to add to a thread.
 | 
			
		||||
///
 | 
			
		||||
/// To be used with [`RoomMessageEventContent::make_reply_to`] or
 | 
			
		||||
/// [`RoomMessageEventContent::make_for_thread`].
 | 
			
		||||
#[derive(Clone, Copy, Debug)]
 | 
			
		||||
pub struct ReplyMetadata<'a> {
 | 
			
		||||
    /// The event ID of the event to reply to.
 | 
			
		||||
    event_id: &'a EventId,
 | 
			
		||||
    /// The sender of the event to reply to.
 | 
			
		||||
    sender: &'a UserId,
 | 
			
		||||
    /// The `m.thread` relation of the event to reply to, if any.
 | 
			
		||||
    thread: Option<&'a Thread>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> ReplyMetadata<'a> {
 | 
			
		||||
    /// Creates a new `ReplyMetadata` with the given event ID, sender and thread relation.
 | 
			
		||||
    pub fn new(event_id: &'a EventId, sender: &'a UserId, thread: Option<&'a Thread>) -> Self {
 | 
			
		||||
        Self { event_id, sender, thread }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> From<&'a OriginalRoomMessageEvent> for ReplyMetadata<'a> {
 | 
			
		||||
    fn from(value: &'a OriginalRoomMessageEvent) -> Self {
 | 
			
		||||
        ReplyMetadata::new(&value.event_id, &value.sender, value.content.thread())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> From<&'a OriginalSyncRoomMessageEvent> for ReplyMetadata<'a> {
 | 
			
		||||
    fn from(value: &'a OriginalSyncRoomMessageEvent) -> Self {
 | 
			
		||||
        ReplyMetadata::new(&value.event_id, &value.sender, value.content.thread())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// The format for the formatted representation of a message body.
 | 
			
		||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
 | 
			
		||||
#[derive(Clone, PartialEq, Eq, StringEnum)]
 | 
			
		||||
 | 
			
		||||
@ -1,179 +0,0 @@
 | 
			
		||||
use std::fmt::{self, Write};
 | 
			
		||||
 | 
			
		||||
use ruma_common::{EventId, RoomId, UserId};
 | 
			
		||||
#[cfg(feature = "html")]
 | 
			
		||||
use ruma_html::Html;
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    sanitize::remove_plain_reply_fallback, FormattedBody, MessageType, OriginalRoomMessageEvent,
 | 
			
		||||
    Relation,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub(super) struct OriginalEventData<'a> {
 | 
			
		||||
    pub(super) body: &'a str,
 | 
			
		||||
    pub(super) formatted: Option<&'a FormattedBody>,
 | 
			
		||||
    pub(super) is_emote: bool,
 | 
			
		||||
    pub(super) is_reply: bool,
 | 
			
		||||
    pub(super) room_id: &'a RoomId,
 | 
			
		||||
    pub(super) event_id: &'a EventId,
 | 
			
		||||
    pub(super) sender: &'a UserId,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> From<&'a OriginalRoomMessageEvent> for OriginalEventData<'a> {
 | 
			
		||||
    fn from(message: &'a OriginalRoomMessageEvent) -> Self {
 | 
			
		||||
        let OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = message;
 | 
			
		||||
        let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. }));
 | 
			
		||||
 | 
			
		||||
        let (body, formatted, is_emote) = match &content.msgtype {
 | 
			
		||||
            MessageType::Audio(_) => ("sent an audio file.", None, false),
 | 
			
		||||
            MessageType::Emote(c) => (&*c.body, c.formatted.as_ref(), true),
 | 
			
		||||
            MessageType::File(_) => ("sent a file.", None, false),
 | 
			
		||||
            MessageType::Image(_) => ("sent an image.", None, false),
 | 
			
		||||
            MessageType::Location(_) => ("sent a location.", None, false),
 | 
			
		||||
            MessageType::Notice(c) => (&*c.body, c.formatted.as_ref(), false),
 | 
			
		||||
            MessageType::ServerNotice(c) => (&*c.body, None, false),
 | 
			
		||||
            MessageType::Text(c) => (&*c.body, c.formatted.as_ref(), false),
 | 
			
		||||
            MessageType::Video(_) => ("sent a video.", None, false),
 | 
			
		||||
            MessageType::VerificationRequest(c) => (&*c.body, None, false),
 | 
			
		||||
            MessageType::_Custom(c) => (&*c.body, None, false),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Self { body, formatted, is_emote, is_reply, room_id, event_id, sender }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_message_quote_fallbacks(original_event: OriginalEventData<'_>) -> (String, String) {
 | 
			
		||||
    let OriginalEventData { body, formatted, is_emote, is_reply, room_id, event_id, sender } =
 | 
			
		||||
        original_event;
 | 
			
		||||
    let emote_sign = is_emote.then_some("* ").unwrap_or_default();
 | 
			
		||||
    let body = is_reply.then(|| remove_plain_reply_fallback(body)).unwrap_or(body);
 | 
			
		||||
    #[cfg(feature = "html")]
 | 
			
		||||
    let html_body = FormattedOrPlainBody { formatted, body, is_reply };
 | 
			
		||||
    #[cfg(not(feature = "html"))]
 | 
			
		||||
    let html_body = FormattedOrPlainBody { 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>"
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct EscapeHtmlEntities<'a>(&'a str);
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for EscapeHtmlEntities<'_> {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        for c in self.0.chars() {
 | 
			
		||||
            // Escape reserved HTML entities and new lines.
 | 
			
		||||
            // <https://developer.mozilla.org/en-US/docs/Glossary/Entity#reserved_characters>
 | 
			
		||||
            match c {
 | 
			
		||||
                '&' => f.write_str("&")?,
 | 
			
		||||
                '<' => f.write_str("<")?,
 | 
			
		||||
                '>' => f.write_str(">")?,
 | 
			
		||||
                '"' => f.write_str(""")?,
 | 
			
		||||
                '\n' => f.write_str("<br>")?,
 | 
			
		||||
                _ => f.write_char(c)?,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct FormattedOrPlainBody<'a> {
 | 
			
		||||
    formatted: Option<&'a FormattedBody>,
 | 
			
		||||
    body: &'a str,
 | 
			
		||||
    #[cfg(feature = "html")]
 | 
			
		||||
    is_reply: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for FormattedOrPlainBody<'_> {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        if let Some(formatted_body) = self.formatted {
 | 
			
		||||
            #[cfg(feature = "html")]
 | 
			
		||||
            if self.is_reply {
 | 
			
		||||
                let html = Html::parse(&formatted_body.body);
 | 
			
		||||
                html.sanitize();
 | 
			
		||||
 | 
			
		||||
                write!(f, "{html}")
 | 
			
		||||
            } else {
 | 
			
		||||
                f.write_str(&formatted_body.body)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #[cfg(not(feature = "html"))]
 | 
			
		||||
            f.write_str(&formatted_body.body)
 | 
			
		||||
        } else {
 | 
			
		||||
            write!(f, "{}", EscapeHtmlEntities(self.body))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get the plain and formatted body for a rich reply.
 | 
			
		||||
///
 | 
			
		||||
/// Returns a `(plain, html)` tuple.
 | 
			
		||||
///
 | 
			
		||||
/// With the `sanitize` feature, [HTML tags and attributes] that are not allowed in the Matrix
 | 
			
		||||
/// spec and previous [rich reply fallbacks] are removed from the previous message in the new rich
 | 
			
		||||
/// reply fallback.
 | 
			
		||||
///
 | 
			
		||||
/// [HTML tags and attributes]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
 | 
			
		||||
/// [rich reply fallbacks]: https://spec.matrix.org/latest/client-server-api/#fallbacks-for-rich-replies
 | 
			
		||||
pub(super) fn plain_and_formatted_reply_body(
 | 
			
		||||
    body: &str,
 | 
			
		||||
    formatted: Option<impl fmt::Display>,
 | 
			
		||||
    original_event: OriginalEventData<'_>,
 | 
			
		||||
) -> (String, String) {
 | 
			
		||||
    let (quoted, quoted_html) = get_message_quote_fallbacks(original_event);
 | 
			
		||||
 | 
			
		||||
    let plain = format!("{quoted}\n\n{body}");
 | 
			
		||||
    let html = match formatted {
 | 
			
		||||
        Some(formatted) => format!("{quoted_html}{formatted}"),
 | 
			
		||||
        None => format!("{quoted_html}{}", EscapeHtmlEntities(body)),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    (plain, html)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use ruma_common::{owned_event_id, owned_room_id, owned_user_id, MilliSecondsSinceUnixEpoch};
 | 
			
		||||
 | 
			
		||||
    use super::OriginalRoomMessageEvent;
 | 
			
		||||
    use crate::{room::message::RoomMessageEventContent, MessageLikeUnsigned};
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn fallback_multiline() {
 | 
			
		||||
        let (plain_quote, html_quote) = super::get_message_quote_fallbacks(
 | 
			
		||||
            (&OriginalRoomMessageEvent {
 | 
			
		||||
                content: RoomMessageEventContent::text_plain("multi\nline"),
 | 
			
		||||
                event_id: owned_event_id!("$1598361704261elfgc:localhost"),
 | 
			
		||||
                sender: owned_user_id!("@alice:example.com"),
 | 
			
		||||
                origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
 | 
			
		||||
                room_id: owned_room_id!("!n8f893n9:example.com"),
 | 
			
		||||
                unsigned: MessageLikeUnsigned::new(),
 | 
			
		||||
            })
 | 
			
		||||
                .into(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assert_eq!(plain_quote, "> <@alice:example.com> multi\n> line");
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            html_quote,
 | 
			
		||||
            "<mx-reply>\
 | 
			
		||||
                <blockquote>\
 | 
			
		||||
                    <a href=\"https://matrix.to/#/!n8f893n9:example.com/$1598361704261elfgc:localhost\">In reply to</a> \
 | 
			
		||||
                    <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>\
 | 
			
		||||
                    <br>\
 | 
			
		||||
                    multi<br>line\
 | 
			
		||||
                </blockquote>\
 | 
			
		||||
            </mx-reply>",
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +1,12 @@
 | 
			
		||||
use as_variant::as_variant;
 | 
			
		||||
use ruma_common::{serde::Raw, OwnedEventId, OwnedUserId, RoomId, UserId};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation,
 | 
			
		||||
    ReplacementMetadata, ReplyWithinThread, RoomMessageEventContent,
 | 
			
		||||
    AddMentions, ForwardThread, MessageType, Relation, ReplacementMetadata, ReplyMetadata,
 | 
			
		||||
    ReplyWithinThread, RoomMessageEventContent,
 | 
			
		||||
};
 | 
			
		||||
use crate::{
 | 
			
		||||
    relation::{InReplyTo, Replacement, Thread},
 | 
			
		||||
    room::message::{reply::OriginalEventData, FormattedBody},
 | 
			
		||||
    AnySyncTimelineEvent, Mentions,
 | 
			
		||||
    Mentions,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Form of [`RoomMessageEventContent`] without relation.
 | 
			
		||||
@ -92,161 +89,81 @@ impl RoomMessageEventContentWithoutRelation {
 | 
			
		||||
        RoomMessageEventContent { msgtype, relates_to, mentions }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Turns `self` into a reply to the given message.
 | 
			
		||||
    /// Turns `self` into a [rich reply] to the message using the given metadata.
 | 
			
		||||
    ///
 | 
			
		||||
    /// 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`,
 | 
			
		||||
    /// and optionally the `rel_type` to `m.thread` if the `original_message is in a thread and
 | 
			
		||||
    /// thread forwarding is enabled.
 | 
			
		||||
    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
 | 
			
		||||
    /// Sets the `in_reply_to` field inside `relates_to`, and optionally the `rel_type` to
 | 
			
		||||
    /// `m.thread` if the metadata has a `thread` and `ForwardThread::Yes` is used.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Panics
 | 
			
		||||
    /// If `AddMentions::Yes` is used, the `sender` in the metadata is added as a user mention.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Panics if `self` has a `formatted_body` with a format other than HTML.
 | 
			
		||||
    /// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies
 | 
			
		||||
    #[track_caller]
 | 
			
		||||
    pub fn make_reply_to(
 | 
			
		||||
    pub fn make_reply_to<'a>(
 | 
			
		||||
        mut self,
 | 
			
		||||
        original_message: &OriginalRoomMessageEvent,
 | 
			
		||||
        metadata: impl Into<ReplyMetadata<'a>>,
 | 
			
		||||
        forward_thread: ForwardThread,
 | 
			
		||||
        add_mentions: AddMentions,
 | 
			
		||||
    ) -> RoomMessageEventContent {
 | 
			
		||||
        self.msgtype.add_reply_fallback(original_message.into());
 | 
			
		||||
        let original_event_id = original_message.event_id.clone();
 | 
			
		||||
        let metadata = metadata.into();
 | 
			
		||||
        let original_event_id = metadata.event_id.to_owned();
 | 
			
		||||
 | 
			
		||||
        let original_thread_id = if forward_thread == ForwardThread::Yes {
 | 
			
		||||
            original_message
 | 
			
		||||
                .content
 | 
			
		||||
                .relates_to
 | 
			
		||||
                .as_ref()
 | 
			
		||||
                .and_then(as_variant!(Relation::Thread))
 | 
			
		||||
                .map(|thread| thread.event_id.clone())
 | 
			
		||||
        let original_thread_id = metadata
 | 
			
		||||
            .thread
 | 
			
		||||
            .filter(|_| forward_thread == ForwardThread::Yes)
 | 
			
		||||
            .map(|thread| thread.event_id.clone());
 | 
			
		||||
        let relates_to = if let Some(event_id) = original_thread_id {
 | 
			
		||||
            Relation::Thread(Thread::plain(event_id.to_owned(), original_event_id.to_owned()))
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
            Relation::Reply { in_reply_to: InReplyTo { event_id: original_event_id.to_owned() } }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let sender_for_mentions =
 | 
			
		||||
            (add_mentions == AddMentions::Yes).then_some(&*original_message.sender);
 | 
			
		||||
 | 
			
		||||
        self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
 | 
			
		||||
        if add_mentions == AddMentions::Yes {
 | 
			
		||||
            self.mentions
 | 
			
		||||
                .get_or_insert_with(Mentions::new)
 | 
			
		||||
                .user_ids
 | 
			
		||||
                .insert(metadata.sender.to_owned());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /// Turns `self` into a reply to the given raw event.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
 | 
			
		||||
    /// quoted version of the `body` of `original_event` (if any). Also sets the `in_reply_to` field
 | 
			
		||||
    /// inside `relates_to`, and optionally the `rel_type` to `m.thread` if the
 | 
			
		||||
    /// `original_message is in a thread and thread forwarding is enabled.
 | 
			
		||||
    ///
 | 
			
		||||
    /// It is recommended to use [`Self::make_reply_to()`] for replies to `m.room.message` events,
 | 
			
		||||
    /// as the generated fallback is better for some `msgtype`s.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Note that except for the panic below, this is infallible. Which means that if a field is
 | 
			
		||||
    /// missing when deserializing the data, the changes that require it will not be applied. It
 | 
			
		||||
    /// will still at least apply the `m.in_reply_to` relation to this content.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Panics
 | 
			
		||||
    ///
 | 
			
		||||
    /// Panics if `self` has a `formatted_body` with a format other than HTML.
 | 
			
		||||
    #[track_caller]
 | 
			
		||||
    pub fn make_reply_to_raw(
 | 
			
		||||
        mut self,
 | 
			
		||||
        original_event: &Raw<AnySyncTimelineEvent>,
 | 
			
		||||
        original_event_id: OwnedEventId,
 | 
			
		||||
        room_id: &RoomId,
 | 
			
		||||
        forward_thread: ForwardThread,
 | 
			
		||||
        add_mentions: AddMentions,
 | 
			
		||||
    ) -> RoomMessageEventContent {
 | 
			
		||||
        #[derive(Deserialize)]
 | 
			
		||||
        struct ContentDeHelper {
 | 
			
		||||
            body: Option<String>,
 | 
			
		||||
            #[serde(flatten)]
 | 
			
		||||
            formatted: Option<FormattedBody>,
 | 
			
		||||
            #[cfg(feature = "unstable-msc1767")]
 | 
			
		||||
            #[serde(rename = "org.matrix.msc1767.text")]
 | 
			
		||||
            text: Option<String>,
 | 
			
		||||
            #[serde(rename = "m.relates_to")]
 | 
			
		||||
            relates_to: Option<crate::room::encrypted::Relation>,
 | 
			
		||||
        self.with_relation(Some(relates_to))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        let sender = original_event.get_field::<OwnedUserId>("sender").ok().flatten();
 | 
			
		||||
        let content = original_event.get_field::<ContentDeHelper>("content").ok().flatten();
 | 
			
		||||
        let relates_to = content.as_ref().and_then(|c| c.relates_to.as_ref());
 | 
			
		||||
 | 
			
		||||
        let content_body = content.as_ref().and_then(|c| {
 | 
			
		||||
            let body = c.body.as_deref();
 | 
			
		||||
            #[cfg(feature = "unstable-msc1767")]
 | 
			
		||||
            let body = body.or(c.text.as_deref());
 | 
			
		||||
 | 
			
		||||
            Some((c, body?))
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Only apply fallback if we managed to deserialize raw event.
 | 
			
		||||
        if let (Some(sender), Some((content, body))) = (&sender, content_body) {
 | 
			
		||||
            let is_reply =
 | 
			
		||||
                matches!(content.relates_to, Some(crate::room::encrypted::Relation::Reply { .. }));
 | 
			
		||||
            let data = OriginalEventData {
 | 
			
		||||
                body,
 | 
			
		||||
                formatted: content.formatted.as_ref(),
 | 
			
		||||
                is_emote: false,
 | 
			
		||||
                is_reply,
 | 
			
		||||
                room_id,
 | 
			
		||||
                event_id: &original_event_id,
 | 
			
		||||
                sender,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            self.msgtype.add_reply_fallback(data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let original_thread_id = if forward_thread == ForwardThread::Yes {
 | 
			
		||||
            relates_to
 | 
			
		||||
                .and_then(as_variant!(crate::room::encrypted::Relation::Thread))
 | 
			
		||||
                .map(|thread| thread.event_id.clone())
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let sender_for_mentions = sender.as_deref().filter(|_| add_mentions == AddMentions::Yes);
 | 
			
		||||
        self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Turns `self` into a new message for a thread, that is optionally a reply.
 | 
			
		||||
    /// Turns `self` into a new message for a [thread], that is optionally a reply.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, this message will be
 | 
			
		||||
    /// in the same thread. If it doesn't, a new thread with `previous_message` as the root is
 | 
			
		||||
    /// created.
 | 
			
		||||
    /// Looks for the `thread` in the given metadata. If it exists, this message will be in the same
 | 
			
		||||
    /// thread. If it doesn't, a new thread is created with the `event_id` in the metadata as the
 | 
			
		||||
    /// root.
 | 
			
		||||
    ///
 | 
			
		||||
    /// If this is a reply within the thread, takes the `body` / `formatted_body` (if any) in `self`
 | 
			
		||||
    /// for the main text and prepends a quoted version of `previous_message`. Also sets the
 | 
			
		||||
    /// `in_reply_to` field inside `relates_to`.
 | 
			
		||||
    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
 | 
			
		||||
    /// It also sets the `in_reply_to` field inside `relates_to` to point the `event_id`
 | 
			
		||||
    /// in the metadata. If `ReplyWithinThread::Yes` is used, the metadata should be constructed
 | 
			
		||||
    /// from the event to make a reply to, otherwise it should be constructed from the latest
 | 
			
		||||
    /// event in the thread.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Panics
 | 
			
		||||
    /// If `AddMentions::Yes` is used, the `sender` in the metadata is added as a user mention.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
 | 
			
		||||
    /// other than HTML.
 | 
			
		||||
    pub fn make_for_thread(
 | 
			
		||||
    /// [thread]: https://spec.matrix.org/latest/client-server-api/#threading
 | 
			
		||||
    pub fn make_for_thread<'a>(
 | 
			
		||||
        self,
 | 
			
		||||
        previous_message: &OriginalRoomMessageEvent,
 | 
			
		||||
        metadata: impl Into<ReplyMetadata<'a>>,
 | 
			
		||||
        is_reply: ReplyWithinThread,
 | 
			
		||||
        add_mentions: AddMentions,
 | 
			
		||||
    ) -> RoomMessageEventContent {
 | 
			
		||||
        let metadata = metadata.into();
 | 
			
		||||
 | 
			
		||||
        let mut content = if is_reply == ReplyWithinThread::Yes {
 | 
			
		||||
            self.make_reply_to(previous_message, ForwardThread::No, add_mentions)
 | 
			
		||||
            self.make_reply_to(metadata, ForwardThread::No, add_mentions)
 | 
			
		||||
        } else {
 | 
			
		||||
            self.into()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) =
 | 
			
		||||
            &previous_message.content.relates_to
 | 
			
		||||
        {
 | 
			
		||||
            event_id.clone()
 | 
			
		||||
        let thread_root = if let Some(Thread { event_id, .. }) = &metadata.thread {
 | 
			
		||||
            event_id.to_owned()
 | 
			
		||||
        } else {
 | 
			
		||||
            previous_message.event_id.clone()
 | 
			
		||||
            metadata.event_id.to_owned()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        content.relates_to = Some(Relation::Thread(Thread {
 | 
			
		||||
            event_id: thread_root,
 | 
			
		||||
            in_reply_to: Some(InReplyTo { event_id: previous_message.event_id.clone() }),
 | 
			
		||||
            in_reply_to: Some(InReplyTo { event_id: metadata.event_id.to_owned() }),
 | 
			
		||||
            is_falling_back: is_reply == ReplyWithinThread::No,
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
@ -262,12 +179,6 @@ impl RoomMessageEventContentWithoutRelation {
 | 
			
		||||
    /// This takes the content and sets it in `m.new_content`, and modifies the `content` to include
 | 
			
		||||
    /// a fallback.
 | 
			
		||||
    ///
 | 
			
		||||
    /// If the message that is replaced is a reply to another message, the latter should also be
 | 
			
		||||
    /// provided to be able to generate a rich reply fallback that takes the `body` /
 | 
			
		||||
    /// `formatted_body` (if any) in `self` for the main text and prepends a quoted version of
 | 
			
		||||
    /// `original_message`.
 | 
			
		||||
    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
 | 
			
		||||
    ///
 | 
			
		||||
    /// If this message contains [`Mentions`], they are copied into `m.new_content` to keep the same
 | 
			
		||||
    /// mentions, but the ones in `content` are filtered with the ones in the
 | 
			
		||||
    /// [`ReplacementMetadata`] so only new mentions will trigger a notification.
 | 
			
		||||
@ -281,7 +192,6 @@ impl RoomMessageEventContentWithoutRelation {
 | 
			
		||||
    pub fn make_replacement(
 | 
			
		||||
        mut self,
 | 
			
		||||
        metadata: impl Into<ReplacementMetadata>,
 | 
			
		||||
        replied_to_message: Option<&OriginalRoomMessageEvent>,
 | 
			
		||||
    ) -> RoomMessageEventContent {
 | 
			
		||||
        let metadata = metadata.into();
 | 
			
		||||
 | 
			
		||||
@ -318,13 +228,7 @@ impl RoomMessageEventContentWithoutRelation {
 | 
			
		||||
 | 
			
		||||
        self.msgtype.make_replacement_body();
 | 
			
		||||
 | 
			
		||||
        // Add reply fallback if needed.
 | 
			
		||||
        let mut content = if let Some(original_message) = replied_to_message {
 | 
			
		||||
            self.make_reply_to(original_message, ForwardThread::No, AddMentions::No)
 | 
			
		||||
        } else {
 | 
			
		||||
            self.into()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let mut content = RoomMessageEventContent::from(self);
 | 
			
		||||
        content.relates_to = Some(relates_to);
 | 
			
		||||
 | 
			
		||||
        content
 | 
			
		||||
@ -341,25 +245,6 @@ impl RoomMessageEventContentWithoutRelation {
 | 
			
		||||
        self.mentions.get_or_insert_with(Mentions::new).add(mentions);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn make_reply_tweaks(
 | 
			
		||||
        mut self,
 | 
			
		||||
        original_event_id: OwnedEventId,
 | 
			
		||||
        original_thread_id: Option<OwnedEventId>,
 | 
			
		||||
        sender_for_mentions: Option<&UserId>,
 | 
			
		||||
    ) -> RoomMessageEventContent {
 | 
			
		||||
        let relates_to = if let Some(event_id) = original_thread_id {
 | 
			
		||||
            Relation::Thread(Thread::plain(event_id.to_owned(), original_event_id.to_owned()))
 | 
			
		||||
        } else {
 | 
			
		||||
            Relation::Reply { in_reply_to: InReplyTo { event_id: original_event_id.to_owned() } }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if let Some(sender) = sender_for_mentions {
 | 
			
		||||
            self.mentions.get_or_insert_with(Mentions::new).user_ids.insert(sender.to_owned());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.with_relation(Some(relates_to))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<MessageType> for RoomMessageEventContentWithoutRelation {
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
use std::{borrow::Cow, collections::BTreeSet};
 | 
			
		||||
use std::borrow::Cow;
 | 
			
		||||
 | 
			
		||||
use assert_matches2::assert_matches;
 | 
			
		||||
use js_int::uint;
 | 
			
		||||
use ruma_common::{
 | 
			
		||||
    mxc_uri, owned_event_id, owned_room_id, owned_user_id, room_id,
 | 
			
		||||
    mxc_uri, owned_event_id, owned_room_id, owned_user_id,
 | 
			
		||||
    serde::{Base64, Raw},
 | 
			
		||||
    user_id, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
 | 
			
		||||
};
 | 
			
		||||
@ -19,7 +19,7 @@ use ruma_events::{
 | 
			
		||||
        },
 | 
			
		||||
        EncryptedFileInit, JsonWebKeyInit, MediaSource,
 | 
			
		||||
    },
 | 
			
		||||
    AnySyncTimelineEvent, EventContent, Mentions, MessageLikeUnsigned, RawExt,
 | 
			
		||||
    EventContent, Mentions, MessageLikeUnsigned, RawExt,
 | 
			
		||||
};
 | 
			
		||||
use serde_json::{
 | 
			
		||||
    from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
 | 
			
		||||
@ -302,155 +302,6 @@ fn content_deserialization_failure() {
 | 
			
		||||
    assert_matches!(from_json_value::<RoomMessageEventContent>(json_data), Err(_));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn escape_tags_in_plain_reply_body() {
 | 
			
		||||
    let first_message = OriginalRoomMessageEvent {
 | 
			
		||||
        content: RoomMessageEventContent::text_plain("Usage: cp <source> <destination>"),
 | 
			
		||||
        event_id: owned_event_id!("$143273582443PhrSn:example.org"),
 | 
			
		||||
        origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
 | 
			
		||||
        room_id: owned_room_id!("!testroomid:example.org"),
 | 
			
		||||
        sender: owned_user_id!("@user:example.org"),
 | 
			
		||||
        unsigned: MessageLikeUnsigned::default(),
 | 
			
		||||
    };
 | 
			
		||||
    let second_message = RoomMessageEventContent::text_plain("Usage: rm <path>").make_reply_to(
 | 
			
		||||
        &first_message,
 | 
			
		||||
        ForwardThread::Yes,
 | 
			
		||||
        AddMentions::No,
 | 
			
		||||
    );
 | 
			
		||||
    assert_matches!(second_message.mentions, None);
 | 
			
		||||
 | 
			
		||||
    assert_matches!(
 | 
			
		||||
        first_message.content.msgtype,
 | 
			
		||||
        MessageType::Text(TextMessageEventContent { body, formatted: None, .. })
 | 
			
		||||
    );
 | 
			
		||||
    assert_eq!(body, "Usage: cp <source> <destination>");
 | 
			
		||||
 | 
			
		||||
    assert_matches!(
 | 
			
		||||
        second_message.msgtype,
 | 
			
		||||
        MessageType::Text(TextMessageEventContent { body, formatted, .. })
 | 
			
		||||
    );
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        body,
 | 
			
		||||
        "\
 | 
			
		||||
        > <@user:example.org> Usage: cp <source> <destination>\n\
 | 
			
		||||
        \n\
 | 
			
		||||
        Usage: rm <path>\
 | 
			
		||||
        "
 | 
			
		||||
    );
 | 
			
		||||
    let formatted = formatted.unwrap();
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        formatted.body,
 | 
			
		||||
        "\
 | 
			
		||||
        <mx-reply>\
 | 
			
		||||
            <blockquote>\
 | 
			
		||||
                <a href=\"https://matrix.to/#/!testroomid:example.org/$143273582443PhrSn:example.org\">In reply to</a> \
 | 
			
		||||
                <a href=\"https://matrix.to/#/@user:example.org\">@user:example.org</a>\
 | 
			
		||||
                <br>\
 | 
			
		||||
                Usage: cp <source> <destination>\
 | 
			
		||||
            </blockquote>\
 | 
			
		||||
        </mx-reply>\
 | 
			
		||||
        Usage: rm <path>\
 | 
			
		||||
        "
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
#[cfg(feature = "html")]
 | 
			
		||||
fn reply_sanitize() {
 | 
			
		||||
    let first_message = OriginalRoomMessageEvent {
 | 
			
		||||
        content: RoomMessageEventContent::text_html(
 | 
			
		||||
            "# This is the first message",
 | 
			
		||||
            "<h1>This is the first message</h1>",
 | 
			
		||||
        ),
 | 
			
		||||
        event_id: owned_event_id!("$143273582443PhrSn:example.org"),
 | 
			
		||||
        origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
 | 
			
		||||
        room_id: owned_room_id!("!testroomid:example.org"),
 | 
			
		||||
        sender: owned_user_id!("@user:example.org"),
 | 
			
		||||
        unsigned: MessageLikeUnsigned::default(),
 | 
			
		||||
    };
 | 
			
		||||
    let second_message = OriginalRoomMessageEvent {
 | 
			
		||||
        content: RoomMessageEventContent::text_html(
 | 
			
		||||
            "This is the _second_ message",
 | 
			
		||||
            "This is the <em>second</em> message",
 | 
			
		||||
        )
 | 
			
		||||
        .make_reply_to(&first_message, ForwardThread::Yes, AddMentions::No),
 | 
			
		||||
        event_id: owned_event_id!("$143273582443PhrSn:example.org"),
 | 
			
		||||
        origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
 | 
			
		||||
        room_id: owned_room_id!("!testroomid:example.org"),
 | 
			
		||||
        sender: owned_user_id!("@user:example.org"),
 | 
			
		||||
        unsigned: MessageLikeUnsigned::default(),
 | 
			
		||||
    };
 | 
			
		||||
    let final_reply = RoomMessageEventContent::text_html(
 | 
			
		||||
        "This is **my** reply",
 | 
			
		||||
        "This is <strong>my</strong> reply",
 | 
			
		||||
    )
 | 
			
		||||
    .make_reply_to(&second_message, ForwardThread::Yes, AddMentions::No);
 | 
			
		||||
 | 
			
		||||
    assert_matches!(
 | 
			
		||||
        first_message.content.msgtype,
 | 
			
		||||
        MessageType::Text(TextMessageEventContent { body, formatted, .. })
 | 
			
		||||
    );
 | 
			
		||||
    assert_eq!(body, "# This is the first message");
 | 
			
		||||
    let formatted = formatted.unwrap();
 | 
			
		||||
    assert_eq!(formatted.body, "<h1>This is the first message</h1>");
 | 
			
		||||
 | 
			
		||||
    assert_matches!(
 | 
			
		||||
        second_message.content.msgtype,
 | 
			
		||||
        MessageType::Text(TextMessageEventContent { body, formatted, .. })
 | 
			
		||||
    );
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        body,
 | 
			
		||||
        "\
 | 
			
		||||
        > <@user:example.org> # This is the first message\n\
 | 
			
		||||
        \n\
 | 
			
		||||
        This is the _second_ message\
 | 
			
		||||
        "
 | 
			
		||||
    );
 | 
			
		||||
    let formatted = formatted.unwrap();
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        formatted.body,
 | 
			
		||||
        "\
 | 
			
		||||
        <mx-reply>\
 | 
			
		||||
            <blockquote>\
 | 
			
		||||
                <a href=\"https://matrix.to/#/!testroomid:example.org/$143273582443PhrSn:example.org\">In reply to</a> \
 | 
			
		||||
                <a href=\"https://matrix.to/#/@user:example.org\">@user:example.org</a>\
 | 
			
		||||
                <br>\
 | 
			
		||||
                <h1>This is the first message</h1>\
 | 
			
		||||
            </blockquote>\
 | 
			
		||||
        </mx-reply>\
 | 
			
		||||
        This is the <em>second</em> message\
 | 
			
		||||
        "
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    assert_matches!(
 | 
			
		||||
        final_reply.msgtype,
 | 
			
		||||
        MessageType::Text(TextMessageEventContent { body, formatted, .. })
 | 
			
		||||
    );
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        body,
 | 
			
		||||
        "\
 | 
			
		||||
        > <@user:example.org> This is the _second_ message\n\
 | 
			
		||||
        \n\
 | 
			
		||||
        This is **my** reply\
 | 
			
		||||
        "
 | 
			
		||||
    );
 | 
			
		||||
    let formatted = formatted.unwrap();
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        formatted.body,
 | 
			
		||||
        "\
 | 
			
		||||
        <mx-reply>\
 | 
			
		||||
            <blockquote>\
 | 
			
		||||
                <a href=\"https://matrix.to/#/!testroomid:example.org/$143273582443PhrSn:example.org\">In reply to</a> \
 | 
			
		||||
                <a href=\"https://matrix.to/#/@user:example.org\">@user:example.org</a>\
 | 
			
		||||
                <br>\
 | 
			
		||||
                This is the <em>second</em> message\
 | 
			
		||||
            </blockquote>\
 | 
			
		||||
        </mx-reply>\
 | 
			
		||||
        This is <strong>my</strong> reply\
 | 
			
		||||
        "
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn reply_thread_fallback() {
 | 
			
		||||
    let thread_root = OriginalRoomMessageEvent {
 | 
			
		||||
@ -569,216 +420,7 @@ fn reply_add_mentions() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn reply_to_raw() {
 | 
			
		||||
    let room_id = room_id!("!roomid:notareal.hs");
 | 
			
		||||
    let event_id = owned_event_id!("$143273582443PhrSn");
 | 
			
		||||
 | 
			
		||||
    let original_message: Raw<AnySyncTimelineEvent> = from_json_value(json!({
 | 
			
		||||
        "content": {
 | 
			
		||||
            "body": "Hello, World!",
 | 
			
		||||
            "msgtype": "m.text",
 | 
			
		||||
        },
 | 
			
		||||
        "event_id": event_id,
 | 
			
		||||
        "origin_server_ts": 134_829_848,
 | 
			
		||||
        "sender": "@user:notareal.hs",
 | 
			
		||||
        "type": "m.room.message",
 | 
			
		||||
    }))
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    let reply = RoomMessageEventContent::text_html(
 | 
			
		||||
        "This is **my** reply",
 | 
			
		||||
        "This is <strong>my</strong> reply",
 | 
			
		||||
    )
 | 
			
		||||
    .make_reply_to_raw(
 | 
			
		||||
        &original_message,
 | 
			
		||||
        event_id.clone(),
 | 
			
		||||
        room_id,
 | 
			
		||||
        ForwardThread::Yes,
 | 
			
		||||
        AddMentions::No,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.relates_to, Some(Relation::Reply { in_reply_to }));
 | 
			
		||||
    assert_eq!(in_reply_to.event_id, event_id);
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.msgtype, MessageType::Text(text_msg));
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        text_msg.body,
 | 
			
		||||
        "> <@user:notareal.hs> Hello, World!\n\
 | 
			
		||||
         \n\
 | 
			
		||||
         This is **my** reply"
 | 
			
		||||
    );
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        text_msg.formatted.unwrap().body,
 | 
			
		||||
        "<mx-reply>\
 | 
			
		||||
            <blockquote>\
 | 
			
		||||
                <a href=\"https://matrix.to/#/!roomid:notareal.hs/$143273582443PhrSn\">In reply to</a> \
 | 
			
		||||
                <a href=\"https://matrix.to/#/@user:notareal.hs\">@user:notareal.hs</a>\
 | 
			
		||||
                <br>\
 | 
			
		||||
                Hello, World!\
 | 
			
		||||
            </blockquote>\
 | 
			
		||||
        </mx-reply>\
 | 
			
		||||
        This is <strong>my</strong> reply"
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn reply_to_raw_no_body() {
 | 
			
		||||
    let room_id = room_id!("!roomid:notareal.hs");
 | 
			
		||||
    let event_id = owned_event_id!("$143273582443PhrSn");
 | 
			
		||||
 | 
			
		||||
    let original_message: Raw<AnySyncTimelineEvent> = from_json_value(json!({
 | 
			
		||||
        "content": {},
 | 
			
		||||
        "event_id": event_id,
 | 
			
		||||
        "origin_server_ts": 134_829_848,
 | 
			
		||||
        "sender": "@user:notareal.hs",
 | 
			
		||||
        "type": "m.room.message",
 | 
			
		||||
    }))
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    let reply = RoomMessageEventContent::text_html(
 | 
			
		||||
        "This is **my** reply",
 | 
			
		||||
        "This is <strong>my</strong> reply",
 | 
			
		||||
    )
 | 
			
		||||
    .make_reply_to_raw(
 | 
			
		||||
        &original_message,
 | 
			
		||||
        event_id.clone(),
 | 
			
		||||
        room_id,
 | 
			
		||||
        ForwardThread::Yes,
 | 
			
		||||
        AddMentions::No,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.relates_to, Some(Relation::Reply { in_reply_to }));
 | 
			
		||||
    assert_eq!(in_reply_to.event_id, event_id);
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.msgtype, MessageType::Text(text_msg));
 | 
			
		||||
    assert_eq!(text_msg.body, "This is **my** reply");
 | 
			
		||||
    assert_eq!(text_msg.formatted.unwrap().body, "This is <strong>my</strong> reply");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn reply_to_raw_no_sender() {
 | 
			
		||||
    let room_id = room_id!("!roomid:notareal.hs");
 | 
			
		||||
    let event_id = owned_event_id!("$143273582443PhrSn");
 | 
			
		||||
 | 
			
		||||
    let original_message: Raw<AnySyncTimelineEvent> = from_json_value(json!({
 | 
			
		||||
        "content": {
 | 
			
		||||
            "body": "Hello, World!",
 | 
			
		||||
            "msgtype": "m.text",
 | 
			
		||||
        },
 | 
			
		||||
        "event_id": event_id,
 | 
			
		||||
        "origin_server_ts": 134_829_848,
 | 
			
		||||
        "type": "m.room.message",
 | 
			
		||||
    }))
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    let reply = RoomMessageEventContent::text_html(
 | 
			
		||||
        "This is **my** reply",
 | 
			
		||||
        "This is <strong>my</strong> reply",
 | 
			
		||||
    )
 | 
			
		||||
    .make_reply_to_raw(
 | 
			
		||||
        &original_message,
 | 
			
		||||
        event_id.clone(),
 | 
			
		||||
        room_id,
 | 
			
		||||
        ForwardThread::Yes,
 | 
			
		||||
        AddMentions::No,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.relates_to, Some(Relation::Reply { in_reply_to }));
 | 
			
		||||
    assert_eq!(in_reply_to.event_id, event_id);
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.msgtype, MessageType::Text(text_msg));
 | 
			
		||||
    assert_eq!(text_msg.body, "This is **my** reply");
 | 
			
		||||
    assert_eq!(text_msg.formatted.unwrap().body, "This is <strong>my</strong> reply");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn reply_to_raw_forward_thread() {
 | 
			
		||||
    let room_id = room_id!("!roomid:notareal.hs");
 | 
			
		||||
    let event_id = owned_event_id!("$143273582443PhrSn");
 | 
			
		||||
 | 
			
		||||
    let original_message: Raw<AnySyncTimelineEvent> = from_json_value(json!({
 | 
			
		||||
        "content": {
 | 
			
		||||
            "m.relates_to": {
 | 
			
		||||
                "rel_type": "m.thread",
 | 
			
		||||
                "event_id": "$threadroot",
 | 
			
		||||
                "m.in_reply_to": {
 | 
			
		||||
                    "event_id": "$repliedto",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        "event_id": event_id,
 | 
			
		||||
        "origin_server_ts": 134_829_848,
 | 
			
		||||
        "sender": "@user:notareal.hs",
 | 
			
		||||
        "type": "m.room.message",
 | 
			
		||||
    }))
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    let reply = RoomMessageEventContent::text_html(
 | 
			
		||||
        "This is **my** reply",
 | 
			
		||||
        "This is <strong>my</strong> reply",
 | 
			
		||||
    )
 | 
			
		||||
    .make_reply_to_raw(
 | 
			
		||||
        &original_message,
 | 
			
		||||
        event_id.clone(),
 | 
			
		||||
        room_id,
 | 
			
		||||
        ForwardThread::Yes,
 | 
			
		||||
        AddMentions::No,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.relates_to, Some(Relation::Thread(thread)));
 | 
			
		||||
    assert_eq!(thread.event_id, "$threadroot");
 | 
			
		||||
    assert_eq!(thread.in_reply_to.unwrap().event_id, event_id);
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.msgtype, MessageType::Text(text_msg));
 | 
			
		||||
    assert_eq!(text_msg.body, "This is **my** reply");
 | 
			
		||||
    assert_eq!(text_msg.formatted.unwrap().body, "This is <strong>my</strong> reply");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn reply_to_raw_add_mentions() {
 | 
			
		||||
    let room_id = room_id!("!roomid:notareal.hs");
 | 
			
		||||
    let event_id = owned_event_id!("$143273582443PhrSn");
 | 
			
		||||
 | 
			
		||||
    let user_id = owned_user_id!("@user:notareal.hs");
 | 
			
		||||
    let other_user_id = owned_user_id!("@other_user:notareal.hs");
 | 
			
		||||
 | 
			
		||||
    let original_message: Raw<AnySyncTimelineEvent> = from_json_value(json!({
 | 
			
		||||
        "content": {
 | 
			
		||||
            "m.mentions": {
 | 
			
		||||
                "user_ids": [other_user_id],
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        "event_id": event_id,
 | 
			
		||||
        "origin_server_ts": 134_829_848,
 | 
			
		||||
        "sender": user_id,
 | 
			
		||||
        "type": "m.room.message",
 | 
			
		||||
    }))
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    let reply = RoomMessageEventContent::text_html(
 | 
			
		||||
        "This is **my** reply",
 | 
			
		||||
        "This is <strong>my</strong> reply",
 | 
			
		||||
    )
 | 
			
		||||
    .make_reply_to_raw(
 | 
			
		||||
        &original_message,
 | 
			
		||||
        event_id.clone(),
 | 
			
		||||
        room_id,
 | 
			
		||||
        ForwardThread::Yes,
 | 
			
		||||
        AddMentions::Yes,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.relates_to, Some(Relation::Reply { in_reply_to }));
 | 
			
		||||
    assert_eq!(in_reply_to.event_id, event_id);
 | 
			
		||||
 | 
			
		||||
    assert_matches!(reply.msgtype, MessageType::Text(text_msg));
 | 
			
		||||
    assert_eq!(text_msg.body, "This is **my** reply");
 | 
			
		||||
    assert_eq!(text_msg.formatted.unwrap().body, "This is <strong>my</strong> reply");
 | 
			
		||||
 | 
			
		||||
    assert_eq!(reply.mentions.unwrap().user_ids, BTreeSet::from([user_id]));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn make_replacement_no_reply() {
 | 
			
		||||
fn make_replacement() {
 | 
			
		||||
    let content = RoomMessageEventContent::text_html(
 | 
			
		||||
        "This is _an edited_ message.",
 | 
			
		||||
        "This is <em>an edited</em> message.",
 | 
			
		||||
@ -798,7 +440,7 @@ fn make_replacement_no_reply() {
 | 
			
		||||
    let original_message: OriginalSyncRoomMessageEvent =
 | 
			
		||||
        from_json_value(original_message_json).unwrap();
 | 
			
		||||
 | 
			
		||||
    let content = content.make_replacement(&original_message, None);
 | 
			
		||||
    let content = content.make_replacement(&original_message);
 | 
			
		||||
 | 
			
		||||
    assert_matches!(
 | 
			
		||||
        content.msgtype,
 | 
			
		||||
@ -810,71 +452,6 @@ fn make_replacement_no_reply() {
 | 
			
		||||
    assert_matches!(content.mentions, None);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn make_replacement_with_reply() {
 | 
			
		||||
    let replied_to_message = OriginalRoomMessageEvent {
 | 
			
		||||
        content: RoomMessageEventContent::text_html(
 | 
			
		||||
            "# This is the first message",
 | 
			
		||||
            "<h1>This is the first message</h1>",
 | 
			
		||||
        ),
 | 
			
		||||
        event_id: owned_event_id!("$143273582443PhrSn:example.org"),
 | 
			
		||||
        origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
 | 
			
		||||
        room_id: owned_room_id!("!testroomid:example.org"),
 | 
			
		||||
        sender: owned_user_id!("@user:example.org"),
 | 
			
		||||
        unsigned: MessageLikeUnsigned::default(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let content = RoomMessageEventContent::text_html(
 | 
			
		||||
        "This is _an edited_ reply.",
 | 
			
		||||
        "This is <em>an edited</em> reply.",
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let original_message_json = json!({
 | 
			
		||||
        "content": {
 | 
			
		||||
            "body": "Hello, World!",
 | 
			
		||||
            "msgtype": "m.text",
 | 
			
		||||
        },
 | 
			
		||||
        "event_id": "$143273582443PhrSn",
 | 
			
		||||
        "origin_server_ts": 134_829_848,
 | 
			
		||||
        "room_id": "!roomid:notareal.hs",
 | 
			
		||||
        "sender": "@user:notareal.hs",
 | 
			
		||||
        "type": "m.room.message",
 | 
			
		||||
    });
 | 
			
		||||
    let original_message: OriginalSyncRoomMessageEvent =
 | 
			
		||||
        from_json_value(original_message_json).unwrap();
 | 
			
		||||
 | 
			
		||||
    let content = content.make_replacement(&original_message, Some(&replied_to_message));
 | 
			
		||||
 | 
			
		||||
    assert_matches!(
 | 
			
		||||
        content.msgtype,
 | 
			
		||||
        MessageType::Text(TextMessageEventContent { body, formatted, .. })
 | 
			
		||||
    );
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        body,
 | 
			
		||||
        "\
 | 
			
		||||
        > <@user:example.org> # This is the first message\n\
 | 
			
		||||
        \n\
 | 
			
		||||
        * This is _an edited_ reply.\
 | 
			
		||||
        "
 | 
			
		||||
    );
 | 
			
		||||
    let formatted = formatted.unwrap();
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        formatted.body,
 | 
			
		||||
        "\
 | 
			
		||||
        <mx-reply>\
 | 
			
		||||
            <blockquote>\
 | 
			
		||||
                <a href=\"https://matrix.to/#/!testroomid:example.org/$143273582443PhrSn:example.org\">In reply to</a> \
 | 
			
		||||
                <a href=\"https://matrix.to/#/@user:example.org\">@user:example.org</a>\
 | 
			
		||||
                <br>\
 | 
			
		||||
                <h1>This is the first message</h1>\
 | 
			
		||||
            </blockquote>\
 | 
			
		||||
        </mx-reply>\
 | 
			
		||||
        * This is <em>an edited</em> reply.\
 | 
			
		||||
        "
 | 
			
		||||
    );
 | 
			
		||||
    assert_matches!(content.mentions, None);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn audio_msgtype_serialization() {
 | 
			
		||||
    let message_event_content =
 | 
			
		||||
@ -1253,7 +830,7 @@ fn add_mentions_then_make_replacement() {
 | 
			
		||||
        "This is <em>an edited</em> message.",
 | 
			
		||||
    );
 | 
			
		||||
    content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()]));
 | 
			
		||||
    content = content.make_replacement(&original_message, None);
 | 
			
		||||
    content = content.make_replacement(&original_message);
 | 
			
		||||
 | 
			
		||||
    let mentions = content.mentions.unwrap();
 | 
			
		||||
    assert_eq!(mentions.user_ids, [bob.clone()].into());
 | 
			
		||||
@ -1287,7 +864,7 @@ fn make_replacement_then_add_mentions() {
 | 
			
		||||
        "This is _an edited_ message.",
 | 
			
		||||
        "This is <em>an edited</em> message.",
 | 
			
		||||
    );
 | 
			
		||||
    content = content.make_replacement(&original_message, None);
 | 
			
		||||
    content = content.make_replacement(&original_message);
 | 
			
		||||
    content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()]));
 | 
			
		||||
 | 
			
		||||
    let mentions = content.mentions.unwrap();
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user