events: Refactor message relations

* Discard unknown relations
* Move new_content into Relation::Replacement
This commit is contained in:
Jonas Platte 2021-06-17 16:28:36 +02:00
parent 2c8af1e17f
commit ec4b719546
No known key found for this signature in database
GPG Key ID: CC154DE0E30B7C67
7 changed files with 299 additions and 250 deletions

View File

@ -52,3 +52,31 @@ impl Relation {
Self { event_id, emoji } Self { event_id, emoji }
} }
} }
#[cfg(test)]
mod tests {
use matches::assert_matches;
use ruma_identifiers::event_id;
use serde_json::{from_value as from_json_value, json};
use super::{ReactionEventContent, Relation};
#[test]
fn deserialize() {
let ev_id = event_id!("$1598361704261elfgc:localhost");
let json = json!({
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": ev_id,
"key": "🦛",
}
});
assert_matches!(
from_json_value::<ReactionEventContent>(json).unwrap(),
ReactionEventContent { relates_to: Relation { event_id, emoji } }
if event_id == ev_id && emoji == "🦛"
);
}
}

View File

@ -7,7 +7,10 @@ use ruma_events_macros::EventContent;
use ruma_identifiers::DeviceIdBox; use ruma_identifiers::DeviceIdBox;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{room::message::Relation, MessageEvent}; use crate::{
room::relationships::{relation_serde, Relation},
MessageEvent,
};
/// An event that has been encrypted. /// An event that has been encrypted.
pub type EncryptedEvent = MessageEvent<EncryptedEventContent>; pub type EncryptedEvent = MessageEvent<EncryptedEventContent>;
@ -35,22 +38,23 @@ pub struct EncryptedEventContent {
#[serde(flatten)] #[serde(flatten)]
pub scheme: EncryptedEventScheme, pub scheme: EncryptedEventScheme,
/// Information about related messages for /// Information about related messages for [rich replies].
/// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies). ///
#[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")] /// [rich replies]: https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies
pub relates_to: Option<Relation>, #[serde(flatten, with = "relation_serde", skip_serializing_if = "Option::is_none")]
pub relation: Option<Relation>,
} }
impl EncryptedEventContent { impl EncryptedEventContent {
/// Creates a new `EncryptedEventContent` with the given scheme and relation. /// Creates a new `EncryptedEventContent` with the given scheme and relation.
pub fn new(scheme: EncryptedEventScheme, relates_to: Option<Relation>) -> Self { pub fn new(scheme: EncryptedEventScheme, relates_to: Option<Relation>) -> Self {
Self { scheme, relates_to } Self { scheme, relation: relates_to }
} }
} }
impl From<EncryptedEventScheme> for EncryptedEventContent { impl From<EncryptedEventScheme> for EncryptedEventContent {
fn from(scheme: EncryptedEventScheme) -> Self { fn from(scheme: EncryptedEventScheme) -> Self {
Self { scheme, relates_to: None } Self { scheme, relation: None }
} }
} }
@ -150,7 +154,7 @@ mod tests {
use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{EncryptedEventContent, EncryptedEventScheme, MegolmV1AesSha2Content}; use super::{EncryptedEventContent, EncryptedEventScheme, MegolmV1AesSha2Content};
use crate::room::message::{InReplyTo, Relation}; use crate::room::relationships::{InReplyTo, Relation};
use ruma_identifiers::event_id; use ruma_identifiers::event_id;
#[test] #[test]
@ -162,7 +166,7 @@ mod tests {
device_id: "device_id".into(), device_id: "device_id".into(),
session_id: "session_id".into(), session_id: "session_id".into(),
}), }),
relates_to: Some(Relation::Reply { relation: Some(Relation::Reply {
in_reply_to: InReplyTo { event_id: event_id!("$h29iv0s8:example.com") }, in_reply_to: InReplyTo { event_id: event_id!("$h29iv0s8:example.com") },
}), }),
}; };
@ -218,7 +222,7 @@ mod tests {
); );
assert_matches!( assert_matches!(
content.relates_to, content.relation,
Some(Relation::Reply { in_reply_to }) Some(Relation::Reply { in_reply_to })
if in_reply_to.event_id == event_id!("$h29iv0s8:example.com") if in_reply_to.event_id == event_id!("$h29iv0s8:example.com")
); );

View File

@ -12,15 +12,13 @@ use ruma_serde::StringEnum;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
#[cfg(feature = "unstable-pre-spec")] use super::{
use super::relationships::{Annotation, Reference, RelationJsonRepr, Replacement}; relationships::{relation_serde, InReplyTo, Relation},
use super::{relationships::RelatesToJsonRepr, EncryptedFile, ImageInfo, ThumbnailInfo}; EncryptedFile, ImageInfo, ThumbnailInfo,
};
#[cfg(feature = "unstable-pre-spec")] #[cfg(feature = "unstable-pre-spec")]
use crate::key::verification::VerificationMethod; use crate::key::verification::VerificationMethod;
// FIXME: Do we want to keep re-exporting this?
pub use super::relationships::InReplyTo;
mod content_serde; mod content_serde;
pub mod feedback; pub mod feedback;
@ -42,29 +40,17 @@ pub struct MessageEventContent {
#[serde(flatten)] #[serde(flatten)]
pub msgtype: MessageType, pub msgtype: MessageType,
/// Information about related messages for /// Information about related messages for [rich replies].
/// [rich replies](https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies).
#[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
pub relates_to: Option<Relation>,
/// New content of an edited message.
/// ///
/// This should only be set if `relates_to` is `Some(Relation::Replacement(_))`. /// [rich replies]: https://matrix.org/docs/spec/client_server/r0.6.1#rich-replies
#[cfg(feature = "unstable-pre-spec")] #[serde(flatten, with = "relation_serde", skip_serializing_if = "Option::is_none")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] pub relates_to: Option<Relation>,
#[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")]
pub new_content: Option<Box<MessageEventContent>>,
} }
impl MessageEventContent { impl MessageEventContent {
/// Create a `MessageEventContent` with the given `MessageType`. /// Create a `MessageEventContent` with the given `MessageType`.
pub fn new(msgtype: MessageType) -> Self { pub fn new(msgtype: MessageType) -> Self {
Self { Self { msgtype, relates_to: None }
msgtype,
relates_to: None,
#[cfg(feature = "unstable-pre-spec")]
new_content: None,
}
} }
/// A constructor to create a plain text message. /// A constructor to create a plain text message.
@ -276,70 +262,6 @@ impl From<MessageType> for MessageEventContent {
} }
} }
/// Enum modeling the different ways relationships can be expressed in a
/// `m.relates_to` field of an `m.room.message` event.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(from = "RelatesToJsonRepr", into = "RelatesToJsonRepr")]
pub enum Relation {
/// A reference to another event.
#[cfg(feature = "unstable-pre-spec")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
Reference(Reference),
/// An annotation to an event.
#[cfg(feature = "unstable-pre-spec")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
Annotation(Annotation),
/// An event that replaces another event.
#[cfg(feature = "unstable-pre-spec")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
Replacement(Replacement),
/// An `m.in_reply_to` relation indicating that the event is a reply to
/// another event.
Reply {
/// Information about another message being replied to.
in_reply_to: InReplyTo,
},
/// Custom, unsupported relation.
#[doc(hidden)]
_Custom(JsonValue),
}
impl From<Relation> for RelatesToJsonRepr {
fn from(value: Relation) -> Self {
match value {
#[cfg(feature = "unstable-pre-spec")]
Relation::Annotation(r) => RelatesToJsonRepr::Relation(RelationJsonRepr::Annotation(r)),
#[cfg(feature = "unstable-pre-spec")]
Relation::Reference(r) => RelatesToJsonRepr::Relation(RelationJsonRepr::Reference(r)),
#[cfg(feature = "unstable-pre-spec")]
Relation::Replacement(r) => {
RelatesToJsonRepr::Relation(RelationJsonRepr::Replacement(r))
}
Relation::Reply { in_reply_to } => RelatesToJsonRepr::Reply { in_reply_to },
Relation::_Custom(c) => RelatesToJsonRepr::Custom(c),
}
}
}
impl From<RelatesToJsonRepr> for Relation {
fn from(value: RelatesToJsonRepr) -> Self {
match value {
#[cfg(feature = "unstable-pre-spec")]
RelatesToJsonRepr::Relation(r) => match r {
RelationJsonRepr::Annotation(a) => Self::Annotation(a),
RelationJsonRepr::Reference(r) => Self::Reference(r),
RelationJsonRepr::Replacement(r) => Self::Replacement(r),
},
RelatesToJsonRepr::Reply { in_reply_to } => Self::Reply { in_reply_to },
RelatesToJsonRepr::Custom(v) => Self::_Custom(v),
}
}
}
/// The payload for an audio message. /// The payload for an audio message.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
@ -1189,3 +1111,36 @@ fn formatted_or_plain_body<'a>(formatted: &'a Option<FormattedBody>, body: &'a s
body body
} }
} }
#[cfg(test)]
mod tests {
use matches::assert_matches;
use ruma_identifiers::event_id;
use serde_json::{from_value as from_json_value, json};
use super::{MessageEventContent, MessageType, Relation};
use crate::room::relationships::InReplyTo;
#[test]
fn deserialize_reply() {
let ev_id = event_id!("$1598361704261elfgc:localhost");
let json = json!({
"msgtype": "m.text",
"body": "<text msg>",
"m.relates_to": {
"m.in_reply_to": {
"event_id": ev_id,
},
},
});
assert_matches!(
from_json_value::<MessageEventContent>(json).unwrap(),
MessageEventContent {
msgtype: MessageType::Text(_),
relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id } }),
} if event_id == ev_id
);
}
}

View File

@ -5,35 +5,20 @@ use serde_json::value::RawValue as RawJsonValue;
use crate::{ use crate::{
from_raw_json_value, from_raw_json_value,
room::message::{MessageEventContent, MessageType, Relation}, room::message::{MessageEventContent, MessageType},
}; };
/// Helper struct to determine the msgtype, relates_to and new_content fields
/// from a `serde_json::value::RawValue`
#[derive(Debug, Deserialize)]
struct MessageContentDeHelper {
#[serde(rename = "m.relates_to")]
relates_to: Option<Relation>,
#[cfg(feature = "unstable-pre-spec")]
#[serde(rename = "m.new_content")]
new_content: Option<Box<MessageEventContent>>,
}
impl<'de> Deserialize<'de> for MessageEventContent { impl<'de> Deserialize<'de> for MessageEventContent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: de::Deserializer<'de>, D: de::Deserializer<'de>,
{ {
let json = Box::<RawJsonValue>::deserialize(deserializer)?; let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let helper = from_raw_json_value::<MessageContentDeHelper, D::Error>(&json)?; let mut deserializer = serde_json::Deserializer::from_str(json.get());
let relation =
super::relation_serde::deserialize(&mut deserializer).map_err(de::Error::custom)?;
Ok(Self { Ok(Self { msgtype: from_raw_json_value(&json)?, relates_to: relation })
msgtype: from_raw_json_value(&json)?,
relates_to: helper.relates_to,
#[cfg(feature = "unstable-pre-spec")]
new_content: helper.new_content,
})
} }
} }

View File

@ -8,45 +8,57 @@
use ruma_identifiers::EventId; use ruma_identifiers::EventId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[cfg(feature = "unstable-pre-spec")]
use crate::room::message::MessageEventContent;
pub(crate) mod relation_serde;
/// Enum modeling the different ways relationships can be expressed in a `m.relates_to` field of an /// Enum modeling the different ways relationships can be expressed in a `m.relates_to` field of an
/// event. /// `m.room.message` or `m.room.encrypted` event.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug)]
#[serde(untagged)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub(crate) enum RelatesToJsonRepr { pub enum Relation {
/// A relation which contains subtypes indicating the type of the relationship with the
/// `rel_type` field.
#[cfg(feature = "unstable-pre-spec")]
Relation(RelationJsonRepr),
/// An `m.in_reply_to` relationship indicating that the event is a reply to another event.
Reply {
/// Information about another message being replied to.
#[serde(rename = "m.in_reply_to")]
in_reply_to: InReplyTo,
},
/// Custom, unsupported relationship.
Custom(JsonValue),
}
/// A relation, which associates new information to an existing event.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg(feature = "unstable-pre-spec")]
#[serde(tag = "rel_type")]
pub(crate) enum RelationJsonRepr {
/// An annotation to an event.
#[serde(rename = "m.annotation")]
Annotation(Annotation),
/// A reference to another event. /// A reference to another event.
#[serde(rename = "m.reference")] #[cfg(feature = "unstable-pre-spec")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
Reference(Reference), Reference(Reference),
/// An annotation to an event.
#[cfg(feature = "unstable-pre-spec")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
Annotation(Annotation),
/// An event that replaces another event. /// An event that replaces another event.
#[serde(rename = "m.replace")] #[cfg(feature = "unstable-pre-spec")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
Replacement(Replacement), Replacement(Replacement),
/// An `m.in_reply_to` relation indicating that the event is a reply to another event.
Reply {
/// Information about another message being replied to.
in_reply_to: InReplyTo,
},
}
/// The event this relation belongs to replaces another event.
#[derive(Clone, Debug)]
#[cfg(feature = "unstable-pre-spec")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
pub struct Replacement {
/// The ID of the event being replacing.
pub event_id: EventId,
/// New content.
pub new_content: Box<MessageEventContent>,
}
#[cfg(feature = "unstable-pre-spec")]
impl Replacement {
/// Creates a new `Replacement` with the given event ID and new content.
pub fn new(event_id: EventId, new_content: Box<MessageEventContent>) -> Self {
Self { event_id, new_content }
}
} }
/// Information about the event a "rich reply" is replying to. /// Information about the event a "rich reply" is replying to.
@ -99,99 +111,3 @@ impl Annotation {
Self { event_id, key } Self { event_id, key }
} }
} }
/// An event replacing another event.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg(feature = "unstable-pre-spec")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Replacement {
/// The event this event is replacing.
pub event_id: EventId,
}
#[cfg(feature = "unstable-pre-spec")]
impl Replacement {
/// Creates a new `Replacement` with the given event ID.
pub fn new(event_id: EventId) -> Self {
Self { event_id }
}
}
#[cfg(test)]
mod tests {
use matches::assert_matches;
use ruma_identifiers::event_id;
use serde_json::{from_value as from_json_value, json};
use crate::room::message::Relation;
#[test]
fn reply_deserialize() {
let event_id = event_id!("$1598361704261elfgc:localhost");
let json = json!({
"m.in_reply_to": {
"event_id": event_id,
}
});
assert_matches!(
from_json_value::<Relation>(json).unwrap(),
Relation::Reply { in_reply_to }
if in_reply_to.event_id == event_id
);
}
#[test]
#[cfg(feature = "unstable-pre-spec")]
fn reference_deserialize() {
let event_id = event_id!("$1598361704261elfgc:localhost");
let json = json!({
"rel_type": "m.reference",
"event_id": event_id,
});
assert_matches!(
from_json_value::<Relation>(json).unwrap(),
Relation::Reference(reference)
if reference.event_id == event_id
);
}
#[test]
#[cfg(feature = "unstable-pre-spec")]
fn replacement_deserialization() {
let event_id = event_id!("$1598361704261elfgc:localhost");
let json = json!({
"rel_type": "m.replace",
"event_id": event_id,
});
assert_matches!(
from_json_value::<Relation>(json).unwrap(),
Relation::Replacement(replacement)
if replacement.event_id == event_id
);
}
#[test]
#[cfg(feature = "unstable-pre-spec")]
fn annotation_deserialize() {
let event_id = event_id!("$1598361704261elfgc:localhost");
let json = json!({
"rel_type": "m.annotation",
"event_id": event_id,
"key": "🦛",
});
assert_matches!(
from_json_value::<Relation>(json).unwrap(),
Relation::Annotation(annotation)
if annotation.event_id == event_id && annotation.key == "🦛"
);
}
}

View File

@ -0,0 +1,162 @@
#[cfg(feature = "unstable-pre-spec")]
use ruma_identifiers::EventId;
use serde::{ser::SerializeStruct as _, Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "unstable-pre-spec")]
use super::{Annotation, Reference, Replacement};
use super::{InReplyTo, Relation};
#[cfg(feature = "unstable-pre-spec")]
use crate::room::message::MessageEventContent;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Relation>, D::Error>
where
D: Deserializer<'de>,
{
fn convert_relation(ev: EventWithRelatesToJsonRepr) -> Option<Relation> {
if let Some(in_reply_to) = ev.relates_to.in_reply_to {
return Some(Relation::Reply { in_reply_to });
}
#[cfg(feature = "unstable-pre-spec")]
if let Some(relation) = ev.relates_to.relation {
let relation = match relation {
RelationJsonRepr::Annotation(a) => Relation::Annotation(a),
RelationJsonRepr::Reference(r) => Relation::Reference(r),
RelationJsonRepr::Replacement(ReplacementJsonRepr { event_id }) => {
let new_content = ev.new_content?;
Relation::Replacement(Replacement { event_id, new_content })
}
// FIXME: Maybe we should log this, though at this point we don't even have access
// to the rel_type of the unknown relation.
RelationJsonRepr::Unknown => return None,
};
return Some(relation);
}
None
}
EventWithRelatesToJsonRepr::deserialize(deserializer).map(convert_relation)
}
pub fn serialize<S>(relation: &Option<Relation>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let relation = match relation {
Some(rel) => rel,
// FIXME: If this crate ends up depending on tracing, emit a warning here.
// This code path should not be reachable due to the skip_serializing_if serde attribute
// that should be applied together with `with = "relation_serde"`.
None => return serializer.serialize_struct("NoRelation", 0)?.end(),
};
let json_repr = match relation {
#[cfg(feature = "unstable-pre-spec")]
Relation::Annotation(r) => EventWithRelatesToJsonRepr::new(RelatesToJsonRepr {
relation: Some(RelationJsonRepr::Annotation(r.clone())),
..Default::default()
}),
#[cfg(feature = "unstable-pre-spec")]
Relation::Reference(r) => EventWithRelatesToJsonRepr::new(RelatesToJsonRepr {
relation: Some(RelationJsonRepr::Reference(r.clone())),
..Default::default()
}),
#[cfg(feature = "unstable-pre-spec")]
Relation::Replacement(Replacement { event_id, new_content }) => {
EventWithRelatesToJsonRepr {
relates_to: RelatesToJsonRepr {
relation: Some(RelationJsonRepr::Replacement(ReplacementJsonRepr {
event_id: event_id.clone(),
})),
..Default::default()
},
new_content: Some(new_content.clone()),
}
}
Relation::Reply { in_reply_to } => EventWithRelatesToJsonRepr::new(RelatesToJsonRepr {
in_reply_to: Some(in_reply_to.clone()),
..Default::default()
}),
};
json_repr.serialize(serializer)
}
#[derive(Deserialize, Serialize)]
struct EventWithRelatesToJsonRepr {
#[serde(rename = "m.relates_to", default, skip_serializing_if = "RelatesToJsonRepr::is_empty")]
relates_to: RelatesToJsonRepr,
#[cfg(feature = "unstable-pre-spec")]
#[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")]
new_content: Option<Box<MessageEventContent>>,
}
impl EventWithRelatesToJsonRepr {
fn new(relates_to: RelatesToJsonRepr) -> Self {
Self {
relates_to,
#[cfg(feature = "unstable-pre-spec")]
new_content: None,
}
}
}
/// Enum modeling the different ways relationships can be expressed in a `m.relates_to` field of an
/// event.
#[derive(Default, Deserialize, Serialize)]
struct RelatesToJsonRepr {
#[serde(rename = "m.in_reply_to", skip_serializing_if = "Option::is_none")]
in_reply_to: Option<InReplyTo>,
#[cfg(feature = "unstable-pre-spec")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
relation: Option<RelationJsonRepr>,
}
impl RelatesToJsonRepr {
fn is_empty(&self) -> bool {
#[cfg(not(feature = "unstable-pre-spec"))]
{
self.in_reply_to.is_none()
}
#[cfg(feature = "unstable-pre-spec")]
{
self.in_reply_to.is_none() && self.relation.is_none()
}
}
}
/// A relation, which associates new information to an existing event.
#[derive(Clone, Deserialize, Serialize)]
#[cfg(feature = "unstable-pre-spec")]
#[serde(tag = "rel_type")]
enum RelationJsonRepr {
/// An annotation to an event.
#[serde(rename = "m.annotation")]
Annotation(Annotation),
/// A reference to another event.
#[serde(rename = "m.reference")]
Reference(Reference),
/// An event that replaces another event.
#[serde(rename = "m.replace")]
Replacement(ReplacementJsonRepr),
/// An unknown relation type.
///
/// Not available in the public API, but exists here so deserialization
/// doesn't fail with new / custom `rel_type`s.
#[serde(other)]
Unknown,
}
#[derive(Clone, Deserialize, Serialize)]
#[cfg(feature = "unstable-pre-spec")]
struct ReplacementJsonRepr {
event_id: EventId,
}

View File

@ -11,10 +11,10 @@ use ruma_events::{
use ruma_events::{ use ruma_events::{
room::{ room::{
message::{ message::{
AudioMessageEventContent, MessageEvent, MessageEventContent, MessageType, Relation, AudioMessageEventContent, MessageEvent, MessageEventContent, MessageType,
TextMessageEventContent, TextMessageEventContent,
}, },
relationships::InReplyTo, relationships::{InReplyTo, Relation},
}, },
Unsigned, Unsigned,
}; };
@ -244,7 +244,7 @@ fn edit_deserialization_061() {
formatted: None, formatted: None,
.. ..
}), }),
relates_to: Some(Relation::_Custom(_)), relates_to: None,
.. ..
} if body == "s/foo/bar" } if body == "s/foo/bar"
); );
@ -277,8 +277,7 @@ fn edit_deserialization_future() {
formatted: None, formatted: None,
.. ..
}), }),
relates_to: Some(Relation::Replacement(Replacement { event_id })), relates_to: Some(Relation::Replacement(Replacement { event_id, new_content })),
new_content: Some(new_content),
.. ..
} if body == "s/foo/bar" } if body == "s/foo/bar"
&& event_id == ev_id && event_id == ev_id