Repository: ruma/ruma (rust)

Issue:
Stabilize threads support
[Spec PR](https://github.com/matrix-org/matrix-spec/pull/1254)

- [ ] [MSC3856](https://github.com/matrix-org/matrix-spec-proposals/pull/3856) removes filters that were introduced in [MSC3440](https://github.com/matrix-org/matrix-spec-proposals/pull/3440).
- [ ] Switch fields to the stable prefix.
- [ ] Remove the `unstable-msc3440` feature.
- [ ] We probably should add an extra `forward_thread` argument to [`RoomMessageEventContent::make_reply_to()`](https://docs.ruma.io/ruma/events/room/message/struct.RoomMessageEventContent.html#method.make_reply_to), like the one in [`RoomMessageEventContent::reply()`](https://docs.ruma.io/ruma/events/room/message/struct.RoomMessageEventContent.html#method.reply). And since now we have to methods that allow to do the same thing, maybe we should deprecate or remove of one them.

Pull request description:
events: Stabilize current support for threads
According to https://github.com/matrix-org/matrix-spec/pull/1254.

Closes #1305.







<!-- Replace -->
----
Preview: https://pr-1314--ruma-docs.surge.sh
<!-- Replace -->


Patch:
diff --git a/crates/ruma-common/CHANGELOG.md b/crates/ruma-common/CHANGELOG.md
index 44f96d9c..efecd6aa 100644
--- a/crates/ruma-common/CHANGELOG.md
+++ b/crates/ruma-common/CHANGELOG.md
@@ -13,6 +13,10 @@ Improvements:
 * Stabilize default room server ACL push rule
 * Stabilize `room_types` in `directory::Filter` and `room_type` in `directory::PublicRoomsChunk`
 * Stabilize support for private read receipts
+* Add stable support for threads
+  * Move `Relation::Thread` and associated types and methods out of `unstable-msc3440`
+  * Add parameter to `RoomMessageEventContent::make_reply_to` to be thread-aware
+  * Add `RoomMessageEventContent::make_for_reply`
 
 # 0.10.3
 
diff --git a/crates/ruma-common/Cargo.toml b/crates/ruma-common/Cargo.toml
index ebf052d3..10316960 100644
--- a/crates/ruma-common/Cargo.toml
+++ b/crates/ruma-common/Cargo.toml
@@ -38,7 +38,6 @@ unstable-msc2870 = []
 unstable-msc3245 = ["unstable-msc3246"]
 unstable-msc3246 = ["unstable-msc3551", "dep:thiserror"]
 unstable-msc3381 = ["unstable-msc1767"]
-unstable-msc3440 = []
 unstable-msc3488 = ["unstable-msc1767"]
 unstable-msc3551 = ["unstable-msc1767"]
 unstable-msc3552 = ["unstable-msc3551"]
diff --git a/crates/ruma-common/src/events/relation.rs b/crates/ruma-common/src/events/relation.rs
index 1f06222f..10aa2cfe 100644
--- a/crates/ruma-common/src/events/relation.rs
+++ b/crates/ruma-common/src/events/relation.rs
@@ -4,17 +4,16 @@
 
 use std::fmt::Debug;
 
-#[cfg(any(feature = "unstable-msc2677", feature = "unstable-msc3440"))]
 use js_int::UInt;
 use serde::{Deserialize, Serialize};
 
-#[cfg(feature = "unstable-msc3440")]
-use super::AnySyncMessageLikeEvent;
-#[cfg(feature = "unstable-msc3440")]
-use crate::serde::Raw;
+use super::AnyMessageLikeEvent;
 #[cfg(any(feature = "unstable-msc2676", feature = "unstable-msc2677"))]
 use crate::MilliSecondsSinceUnixEpoch;
-use crate::{serde::StringEnum, PrivOwnedStr};
+use crate::{
+    serde::{Raw, StringEnum},
+    PrivOwnedStr,
+};
 #[cfg(feature = "unstable-msc2676")]
 use crate::{OwnedEventId, OwnedUserId};
 
@@ -112,11 +111,10 @@ impl BundledReplacement {
 
 /// A bundled thread.
 #[derive(Clone, Debug, Deserialize, Serialize)]
-#[cfg(feature = "unstable-msc3440")]
 #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
 pub struct BundledThread {
     /// The latest event in the thread.
-    pub latest_event: Box<Raw<AnySyncMessageLikeEvent>>,
+    pub latest_event: Box<Raw<AnyMessageLikeEvent>>,
 
     /// The number of events in the thread.
     pub count: UInt,
@@ -125,11 +123,10 @@ pub struct BundledThread {
     pub current_user_participated: bool,
 }
 
-#[cfg(feature = "unstable-msc3440")]
 impl BundledThread {
     /// Creates a new `BundledThread` with the given event, count and user participated flag.
     pub fn new(
-        latest_event: Box<Raw<AnySyncMessageLikeEvent>>,
+        latest_event: Box<Raw<AnyMessageLikeEvent>>,
         count: UInt,
         current_user_participated: bool,
     ) -> Self {
@@ -154,8 +151,7 @@ pub struct Relations {
     pub replace: Option<BundledReplacement>,
 
     /// Thread relation.
-    #[cfg(feature = "unstable-msc3440")]
-    #[serde(rename = "io.element.thread", alias = "m.thread")]
+    #[serde(rename = "m.thread")]
     pub thread: Option<BundledThread>,
 }
 
@@ -182,8 +178,7 @@ pub enum RelationType {
     Replacement,
 
     /// `m.thread`, a participant to a thread.
-    #[cfg(feature = "unstable-msc3440")]
-    #[ruma_enum(rename = "io.element.thread", alias = "m.thread")]
+    #[ruma_enum(rename = "m.thread")]
     Thread,
 
     #[doc(hidden)]
diff --git a/crates/ruma-common/src/events/room/encrypted.rs b/crates/ruma-common/src/events/room/encrypted.rs
index 5f6f5e2e..62149fe4 100644
--- a/crates/ruma-common/src/events/room/encrypted.rs
+++ b/crates/ruma-common/src/events/room/encrypted.rs
@@ -102,7 +102,6 @@ pub enum Relation {
     Annotation(Annotation),
 
     /// An event that belongs to a thread.
-    #[cfg(feature = "unstable-msc3440")]
     Thread(Thread),
 
     #[doc(hidden)]
@@ -117,7 +116,6 @@ impl From<message::Relation> for Relation {
             message::Relation::Replacement(re) => {
                 Self::Replacement(Replacement { event_id: re.event_id })
             }
-            #[cfg(feature = "unstable-msc3440")]
             message::Relation::Thread(t) => Self::Thread(Thread {
                 event_id: t.event_id,
                 in_reply_to: t.in_reply_to,
@@ -178,7 +176,6 @@ impl Annotation {
 
 /// A thread relation for an event.
 #[derive(Clone, Debug, Deserialize, Serialize)]
-#[cfg(feature = "unstable-msc3440")]
 #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
 pub struct Thread {
     /// The ID of the root message in the thread.
@@ -199,7 +196,6 @@ pub struct Thread {
     pub is_falling_back: bool,
 }
 
-#[cfg(feature = "unstable-msc3440")]
 impl Thread {
     /// Convenience method to create a regular `Thread` with the given event ID and latest
     /// message-like event ID.
diff --git a/crates/ruma-common/src/events/room/encrypted/relation_serde.rs b/crates/ruma-common/src/events/room/encrypted/relation_serde.rs
index e12287ef..ecea6027 100644
--- a/crates/ruma-common/src/events/room/encrypted/relation_serde.rs
+++ b/crates/ruma-common/src/events/room/encrypted/relation_serde.rs
@@ -4,10 +4,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use super::Annotation;
 #[cfg(feature = "unstable-msc2676")]
 use super::Replacement;
-#[cfg(feature = "unstable-msc3440")]
-use super::Thread;
-use super::{InReplyTo, Reference, Relation};
-#[cfg(feature = "unstable-msc3440")]
+use super::{InReplyTo, Reference, Relation, Thread};
 use crate::OwnedEventId;
 
 impl<'de> Deserialize<'de> for Relation {
@@ -17,7 +14,6 @@ impl<'de> Deserialize<'de> for Relation {
     {
         let ev = EventWithRelatesToJsonRepr::deserialize(deserializer)?;
 
-        #[cfg(feature = "unstable-msc3440")]
         if let Some(
             RelationJsonRepr::ThreadStable(ThreadStableJsonRepr { event_id, is_falling_back })
             | RelationJsonRepr::ThreadUnstable(ThreadUnstableJsonRepr { event_id, is_falling_back }),
@@ -40,7 +36,6 @@ impl<'de> Deserialize<'de> for Relation {
                 RelationJsonRepr::Replacement(Replacement { event_id }) => {
                     Relation::Replacement(Replacement { event_id })
                 }
-                #[cfg(feature = "unstable-msc3440")]
                 RelationJsonRepr::ThreadStable(_) | RelationJsonRepr::ThreadUnstable(_) => {
                     unreachable!()
                 }
@@ -80,11 +75,10 @@ impl Serialize for Relation {
             Relation::Reply { in_reply_to } => {
                 RelatesToJsonRepr { in_reply_to: Some(in_reply_to.clone()), ..Default::default() }
             }
-            #[cfg(feature = "unstable-msc3440")]
             Relation::Thread(Thread { event_id, in_reply_to, is_falling_back }) => {
                 RelatesToJsonRepr {
                     in_reply_to: Some(in_reply_to.clone()),
-                    relation: Some(RelationJsonRepr::ThreadUnstable(ThreadUnstableJsonRepr {
+                    relation: Some(RelationJsonRepr::ThreadStable(ThreadStableJsonRepr {
                         event_id: event_id.clone(),
                         is_falling_back: *is_falling_back,
                     })),
@@ -123,7 +117,6 @@ impl RelatesToJsonRepr {
 
 /// A thread relation without the reply fallback, with stable names.
 #[derive(Clone, Deserialize, Serialize)]
-#[cfg(feature = "unstable-msc3440")]
 struct ThreadStableJsonRepr {
     /// The ID of the root message in the thread.
     pub event_id: OwnedEventId,
@@ -136,7 +129,6 @@ struct ThreadStableJsonRepr {
 
 /// A thread relation without the reply fallback, with unstable names.
 #[derive(Clone, Deserialize, Serialize)]
-#[cfg(feature = "unstable-msc3440")]
 struct ThreadUnstableJsonRepr {
     /// The ID of the root message in the thread.
     pub event_id: OwnedEventId,
@@ -170,12 +162,10 @@ enum RelationJsonRepr {
     Replacement(Replacement),
 
     /// An event that belongs to a thread, with stable names.
-    #[cfg(feature = "unstable-msc3440")]
     #[serde(rename = "m.thread")]
     ThreadStable(ThreadStableJsonRepr),
 
     /// An event that belongs to a thread, with unstable names.
-    #[cfg(feature = "unstable-msc3440")]
     #[serde(rename = "io.element.thread")]
     ThreadUnstable(ThreadUnstableJsonRepr),
 
diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs
index 8220f146..de2b2069 100644
--- a/crates/ruma-common/src/events/room/message.rs
+++ b/crates/ruma-common/src/events/room/message.rs
@@ -106,14 +106,20 @@ impl RoomMessageEventContent {
     /// Turns `self` into a reply to the given message.
     ///
     /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
-    /// quoted version of `original_message`. Also sets the `in_reply_to` field inside `relates_to`.
+    /// 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"))]
     ///
     /// # Panics
     ///
     /// Panics if `self` has a `formatted_body` with a format other than HTML.
     #[track_caller]
-    pub fn make_reply_to(mut self, original_message: &OriginalRoomMessageEvent) -> Self {
+    pub fn make_reply_to(
+        mut self,
+        original_message: &OriginalRoomMessageEvent,
+        forward_thread: ForwardThread,
+    ) -> Self {
         let empty_formatted_body = || FormattedBody::html(String::new());
 
         let (body, formatted) = {
@@ -154,99 +160,46 @@ impl RoomMessageEventContent {
             );
         }
 
-        self.relates_to = Some(Relation::Reply {
-            in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() },
-        });
-
-        self
-    }
-
-    /// Create a new reply with the given message and optionally forwards the [`Relation::Thread`].
-    ///
-    /// If `message` is a text, an emote or a notice message, it is modified to include the rich
-    /// reply fallback.
-    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
-    #[cfg(feature = "unstable-msc3440")]
-    pub fn reply(
-        message: MessageType,
-        original_message: &OriginalRoomMessageEvent,
-        forward_thread: ForwardThread,
-    ) -> Self {
-        let make_reply = |body, formatted: Option<FormattedBody>| {
-            reply::plain_and_formatted_reply_body(body, formatted.map(|f| f.body), original_message)
-        };
-
-        let msgtype = match message {
-            MessageType::Text(TextMessageEventContent { body, formatted, .. }) => {
-                let (body, html_body) = make_reply(body, formatted);
-                MessageType::Text(TextMessageEventContent::html(body, html_body))
-            }
-            MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => {
-                let (body, html_body) = make_reply(body, formatted);
-                MessageType::Emote(EmoteMessageEventContent::html(body, html_body))
-            }
-            MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => {
-                let (body, html_body) = make_reply(body, formatted);
-                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()))
+            Relation::Thread(Thread::plain(event_id.clone(), original_message.event_id.clone()))
         } else {
             Relation::Reply {
                 in_reply_to: InReplyTo { event_id: original_message.event_id.clone() },
             }
         };
+        self.relates_to = Some(relates_to);
 
-        Self { msgtype, relates_to: Some(relates_to) }
+        self
     }
 
-    /// Create 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, a message for the same
-    /// thread is created. If it doesn't, a new thread with `previous_message` as the root is
+    /// 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 `message` is a text, an emote or a notice message, and this is a reply in the thread, it
-    /// is modified to include the rich reply fallback.
+    /// 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"))]
-    #[cfg(feature = "unstable-msc3440")]
-    pub fn for_thread(
-        message: MessageType,
+    ///
+    /// # 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(
+        mut self,
         previous_message: &OriginalRoomMessageEvent,
-        is_reply: ReplyInThread,
+        is_reply: ReplyWithinThread,
     ) -> Self {
-        let make_reply = |body, formatted: Option<FormattedBody>| {
-            reply::plain_and_formatted_reply_body(body, formatted.map(|f| f.body), previous_message)
-        };
-
-        let msgtype = if is_reply == ReplyInThread::Yes {
-            // If this is a real reply, add the rich reply fallback.
-            match message {
-                MessageType::Text(TextMessageEventContent { body, formatted, .. }) => {
-                    let (body, html_body) = make_reply(body, formatted);
-                    MessageType::Text(TextMessageEventContent::html(body, html_body))
-                }
-                MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => {
-                    let (body, html_body) = make_reply(body, formatted);
-                    MessageType::Emote(EmoteMessageEventContent::html(body, html_body))
-                }
-                MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => {
-                    let (body, html_body) = make_reply(body, formatted);
-                    MessageType::Notice(NoticeMessageEventContent::html(body, html_body))
-                }
-                _ => message,
-            }
-        } else {
-            message
-        };
+        if is_reply == ReplyWithinThread::Yes {
+            self = self.make_reply_to(previous_message, ForwardThread::No);
+        }
 
         let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) =
             &previous_message.content.relates_to
@@ -256,14 +209,13 @@ impl RoomMessageEventContent {
             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,
-            })),
-        }
+        self.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 == ReplyWithinThread::No,
+        }));
+
+        self
     }
 
     /// Returns a reference to the `msgtype` string.
@@ -314,15 +266,15 @@ impl 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]).
+    /// This should be set if your client doesn't render threads (see the [info
+    /// box for clients which are acutely aware of threads]).
     ///
-    /// [MSC3440]: https://github.com/matrix-org/matrix-spec-proposals/pull/3440
+    /// [info box for clients which are acutely aware of threads]: https://spec.matrix.org/v1.4/client-server-api/#fallback-for-unthreaded-clients
     Yes,
 
     /// Create a reply in the main conversation even if the original message is in a thread.
@@ -332,20 +284,21 @@ pub enum ForwardThread {
 }
 
 /// 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 {
+pub enum ReplyWithinThread {
     /// This is a reply.
     ///
-    /// Create a proper reply _in_ the thread.
+    /// Create a [reply within the thread].
+    ///
+    /// [reply within the thread]: https://spec.matrix.org/v1.4/client-server-api/#replies-within-threads
     Yes,
 
     /// This is not a reply.
     ///
-    /// Create a regular message in the thread, with a reply fallback, according to [MSC3440].
+    /// Create a regular message in the thread, with a [fallback for unthreaded clients].
     ///
-    /// [MSC3440]: https://github.com/matrix-org/matrix-spec-proposals/pull/3440
+    /// [fallback for unthreaded clients]: https://spec.matrix.org/v1.4/client-server-api/#fallback-for-unthreaded-clients
     No,
 }
 
@@ -520,7 +473,6 @@ pub enum Relation {
     Replacement(Replacement),
 
     /// An event that belongs to a thread.
-    #[cfg(feature = "unstable-msc3440")]
     Thread(Thread),
 
     #[doc(hidden)]
@@ -564,7 +516,6 @@ impl Replacement {
 
 /// The content of a thread relation.
 #[derive(Clone, Debug, Deserialize, Serialize)]
-#[cfg(feature = "unstable-msc3440")]
 #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
 pub struct Thread {
     /// The ID of the root message in the thread.
@@ -585,7 +536,6 @@ pub struct Thread {
     pub is_falling_back: bool,
 }
 
-#[cfg(feature = "unstable-msc3440")]
 impl Thread {
     /// Convenience method to create a regular `Thread` with the given event ID and latest
     /// message-like event ID.
diff --git a/crates/ruma-common/src/events/room/message/relation_serde.rs b/crates/ruma-common/src/events/room/message/relation_serde.rs
index c68d4b67..1afd392d 100644
--- a/crates/ruma-common/src/events/room/message/relation_serde.rs
+++ b/crates/ruma-common/src/events/room/message/relation_serde.rs
@@ -4,10 +4,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use super::Replacement;
 #[cfg(feature = "unstable-msc2676")]
 use super::RoomMessageEventContent;
-#[cfg(feature = "unstable-msc3440")]
-use super::Thread;
-use super::{InReplyTo, Relation};
-#[cfg(any(feature = "unstable-msc2676", feature = "unstable-msc3440"))]
+use super::{InReplyTo, Relation, Thread};
 use crate::OwnedEventId;
 
 impl<'de> Deserialize<'de> for Relation {
@@ -17,7 +14,6 @@ impl<'de> Deserialize<'de> for Relation {
     {
         let ev = EventWithRelatesToJsonRepr::deserialize(deserializer)?;
 
-        #[cfg(feature = "unstable-msc3440")]
         if let Some(
             RelationJsonRepr::ThreadStable(ThreadStableJsonRepr { event_id, is_falling_back })
             | RelationJsonRepr::ThreadUnstable(ThreadUnstableJsonRepr { event_id, is_falling_back }),
@@ -44,7 +40,6 @@ impl<'de> Deserialize<'de> for Relation {
                 // 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 => Relation::_Custom,
-                #[cfg(feature = "unstable-msc3440")]
                 RelationJsonRepr::ThreadStable(_) | RelationJsonRepr::ThreadUnstable(_) => {
                     unreachable!()
                 }
@@ -80,11 +75,10 @@ impl Serialize for Relation {
                     new_content: Some(new_content.clone()),
                 }
             }
-            #[cfg(feature = "unstable-msc3440")]
             Relation::Thread(Thread { event_id, in_reply_to, is_falling_back }) => {
                 EventWithRelatesToJsonRepr::new(RelatesToJsonRepr {
                     in_reply_to: Some(in_reply_to.clone()),
-                    relation: Some(RelationJsonRepr::ThreadUnstable(ThreadUnstableJsonRepr {
+                    relation: Some(RelationJsonRepr::ThreadStable(ThreadStableJsonRepr {
                         event_id: event_id.clone(),
                         is_falling_back: *is_falling_back,
                     })),
@@ -144,13 +138,11 @@ enum RelationJsonRepr {
     #[serde(rename = "m.replace")]
     Replacement(ReplacementJsonRepr),
 
-    /// An event that belongs to a thread, with unstable names.
-    #[cfg(feature = "unstable-msc3440")]
+    /// An event that belongs to a thread, with stable names.
     #[serde(rename = "m.thread")]
     ThreadStable(ThreadStableJsonRepr),
 
     /// An event that belongs to a thread, with unstable names.
-    #[cfg(feature = "unstable-msc3440")]
     #[serde(rename = "io.element.thread")]
     ThreadUnstable(ThreadUnstableJsonRepr),
 
@@ -170,7 +162,6 @@ struct ReplacementJsonRepr {
 
 /// A thread relation without the reply fallback, with stable names.
 #[derive(Clone, Deserialize, Serialize)]
-#[cfg(feature = "unstable-msc3440")]
 struct ThreadStableJsonRepr {
     /// The ID of the root message in the thread.
     event_id: OwnedEventId,
@@ -183,7 +174,6 @@ struct ThreadStableJsonRepr {
 
 /// A thread relation without the reply fallback, with unstable names.
 #[derive(Clone, Deserialize, Serialize)]
-#[cfg(feature = "unstable-msc3440")]
 struct ThreadUnstableJsonRepr {
     /// The ID of the root message in the thread.
     event_id: OwnedEventId,
diff --git a/crates/ruma/Cargo.toml b/crates/ruma/Cargo.toml
index 317d8727..410076f2 100644
--- a/crates/ruma/Cargo.toml
+++ b/crates/ruma/Cargo.toml
@@ -141,7 +141,6 @@ unstable-msc2967 = ["ruma-client-api?/unstable-msc2967"]
 unstable-msc3245 = ["ruma-common/unstable-msc3245"]
 unstable-msc3246 = ["ruma-common/unstable-msc3246"]
 unstable-msc3381 = ["ruma-common/unstable-msc3381"]
-unstable-msc3440 = ["ruma-common/unstable-msc3440"]
 unstable-msc3488 = [
     "ruma-client-api?/unstable-msc3488",
     "ruma-common/unstable-msc3488",
@@ -179,7 +178,6 @@ __ci = [
     "unstable-msc3245",
     "unstable-msc3246",
     "unstable-msc3381",
-    "unstable-msc3440",
     "unstable-msc3488",
     "unstable-msc3551",
     "unstable-msc3552",


Test patch:
diff --git a/crates/ruma-common/tests/events/relations.rs b/crates/ruma-common/tests/events/relations.rs
index e65297c2..d53736af 100644
--- a/crates/ruma-common/tests/events/relations.rs
+++ b/crates/ruma-common/tests/events/relations.rs
@@ -149,7 +149,6 @@ fn replacement_deserialize() {
 }
 
 #[test]
-#[cfg(feature = "unstable-msc3440")]
 fn thread_plain_serialize() {
     use ruma_common::events::room::message::Thread;
 
@@ -172,12 +171,12 @@ fn thread_plain_serialize() {
             "msgtype": "m.text",
             "body": "<text msg>",
             "m.relates_to": {
-                "rel_type": "io.element.thread",
+                "rel_type": "m.thread",
                 "event_id": "$1598361704261elfgc",
                 "m.in_reply_to": {
                     "event_id": "$latesteventid",
                 },
-                "io.element.show_reply": true,
+                "is_falling_back": true,
             },
         })
     );
@@ -190,19 +189,18 @@ fn thread_plain_serialize() {
             "body": "<text msg>",
             "org.matrix.msc1767.text": "<text msg>",
             "m.relates_to": {
-                "rel_type": "io.element.thread",
+                "rel_type": "m.thread",
                 "event_id": "$1598361704261elfgc",
                 "m.in_reply_to": {
                     "event_id": "$latesteventid",
                 },
-                "io.element.show_reply": true,
+                "is_falling_back": true,
             },
         })
     );
 }
 
 #[test]
-#[cfg(feature = "unstable-msc3440")]
 fn thread_reply_serialize() {
     use ruma_common::events::room::message::Thread;
 
@@ -225,7 +223,7 @@ fn thread_reply_serialize() {
             "msgtype": "m.text",
             "body": "<text msg>",
             "m.relates_to": {
-                "rel_type": "io.element.thread",
+                "rel_type": "m.thread",
                 "event_id": "$1598361704261elfgc",
                 "m.in_reply_to": {
                     "event_id": "$repliedtoeventid",
@@ -242,7 +240,7 @@ fn thread_reply_serialize() {
             "body": "<text msg>",
             "org.matrix.msc1767.text": "<text msg>",
             "m.relates_to": {
-                "rel_type": "io.element.thread",
+                "rel_type": "m.thread",
                 "event_id": "$1598361704261elfgc",
                 "m.in_reply_to": {
                     "event_id": "$repliedtoeventid",
@@ -253,7 +251,6 @@ fn thread_reply_serialize() {
 }
 
 #[test]
-#[cfg(feature = "unstable-msc3440")]
 fn thread_stable_deserialize() {
     let json = json!({
         "msgtype": "m.text",
@@ -281,7 +278,6 @@ fn thread_stable_deserialize() {
 }
 
 #[test]
-#[cfg(feature = "unstable-msc3440")]
 fn thread_unstable_deserialize() {
     let json = json!({
         "msgtype": "m.text",
diff --git a/crates/ruma-common/tests/events/room_message.rs b/crates/ruma-common/tests/events/room_message.rs
index 506ab87e..71a43d50 100644
--- a/crates/ruma-common/tests/events/room_message.rs
+++ b/crates/ruma-common/tests/events/room_message.rs
@@ -486,7 +486,7 @@ fn content_deserialization_failure() {
 #[test]
 #[cfg(feature = "unstable-sanitize")]
 fn reply_sanitize() {
-    use ruma_common::events::room::message::TextMessageEventContent;
+    use ruma_common::events::room::message::{ForwardThread, TextMessageEventContent};
 
     let first_message = OriginalRoomMessageEvent {
         content: RoomMessageEventContent::text_html(
@@ -504,7 +504,7 @@ fn reply_sanitize() {
             "This is the _second_ message",
             "This is the <em>second</em> message",
         )
-        .make_reply_to(&first_message),
+        .make_reply_to(&first_message, ForwardThread::Yes),
         event_id: event_id!("$143273582443PhrSn:example.org").to_owned(),
         origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
         room_id: room_id!("!testroomid:example.org").to_owned(),
@@ -515,7 +515,7 @@ fn reply_sanitize() {
         "This is **my** reply",
         "This is <strong>my</strong> reply",
     )
-    .make_reply_to(&second_message);
+    .make_reply_to(&second_message, ForwardThread::Yes);
 
     let (body, formatted) = assert_matches!(
         first_message.content.msgtype,