From dca268e67a0c44ccd8659c8fa2ed40fa1b8efa34 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germains@element.io>
Date: Wed, 1 Sep 2021 10:55:47 +0100
Subject: [PATCH 1/3] Replace eventIsReply util with replyEventId getter

---
 src/components/views/rooms/EditMessageComposer.tsx | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx
index b7e067ee93..7a3767deb7 100644
--- a/src/components/views/rooms/EditMessageComposer.tsx
+++ b/src/components/views/rooms/EditMessageComposer.tsx
@@ -43,11 +43,6 @@ import QuestionDialog from "../dialogs/QuestionDialog";
 import { ActionPayload } from "../../../dispatcher/payloads";
 import AccessibleButton from '../elements/AccessibleButton';
 
-function eventIsReply(mxEvent: MatrixEvent): boolean {
-    const relatesTo = mxEvent.getContent()["m.relates_to"];
-    return !!(relatesTo && relatesTo["m.in_reply_to"]);
-}
-
 function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
     const html = mxEvent.getContent().formatted_body;
     if (!html) {
@@ -72,7 +67,7 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
     if (isEmote) {
         model = stripEmoteCommand(model);
     }
-    const isReply = eventIsReply(editedEvent);
+    const isReply = !!editedEvent.replyEventId;
     let plainPrefix = "";
     let htmlPrefix = "";
 

From 95d1b06abb4ad612bd00ee5569b4dd85269ddde3 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germains@element.io>
Date: Wed, 1 Sep 2021 12:12:40 +0100
Subject: [PATCH 2/3] Make composer able to reply in thread or in room timeline

---
 src/components/structures/ThreadView.tsx      |  1 +
 src/components/views/elements/ReplyThread.tsx |  4 ++-
 .../views/rooms/MessageComposer.tsx           |  3 +++
 .../views/rooms/SendMessageComposer.tsx       | 27 ++++++++++++++-----
 .../views/rooms/SendMessageComposer-test.js   |  8 +++---
 5 files changed, 31 insertions(+), 12 deletions(-)

diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx
index a2595debc8..94f3f26261 100644
--- a/src/components/structures/ThreadView.tsx
+++ b/src/components/structures/ThreadView.tsx
@@ -136,6 +136,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
                 <MessageComposer
                     room={this.props.room}
                     resizeNotifier={this.props.resizeNotifier}
+                    replyInThread={true}
                     replyToEvent={this.state?.thread?.replyToEvent}
                     showReplyPreview={false}
                     permalinkCreator={this.props.permalinkCreator}
diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx
index 0eb795e257..d5b6af17f2 100644
--- a/src/components/views/elements/ReplyThread.tsx
+++ b/src/components/views/elements/ReplyThread.tsx
@@ -19,6 +19,7 @@ import React from 'react';
 import { _t } from '../../../languageHandler';
 import dis from '../../../dispatcher/dispatcher';
 import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { UNSTABLE_ELEMENT_REPLY_IN_THREAD } from "matrix-js-sdk/src/@types/event";
 import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
 import SettingsStore from "../../../settings/SettingsStore";
 import { Layout } from "../../../settings/Layout";
@@ -206,12 +207,13 @@ export default class ReplyThread extends React.Component<IProps, IState> {
         return { body, html };
     }
 
-    public static makeReplyMixIn(ev: MatrixEvent) {
+    public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) {
         if (!ev) return {};
         return {
             'm.relates_to': {
                 'm.in_reply_to': {
                     'event_id': ev.getId(),
+                    [UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]: replyInThread,
                 },
             },
         };
diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx
index fbf3b58570..466675ac64 100644
--- a/src/components/views/rooms/MessageComposer.tsx
+++ b/src/components/views/rooms/MessageComposer.tsx
@@ -183,6 +183,7 @@ interface IProps {
     resizeNotifier: ResizeNotifier;
     permalinkCreator: RoomPermalinkCreator;
     replyToEvent?: MatrixEvent;
+    replyInThread?: boolean;
     showReplyPreview?: boolean;
     e2eStatus?: E2EStatus;
     compact?: boolean;
@@ -204,6 +205,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
     private voiceRecordingButton: VoiceRecordComposerTile;
 
     static defaultProps = {
+        replyInThread: false,
         showReplyPreview: true,
         compact: false,
     };
@@ -383,6 +385,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
                     room={this.props.room}
                     placeholder={this.renderPlaceholderText()}
                     permalinkCreator={this.props.permalinkCreator}
+                    replyInThread={this.props.replyInThread}
                     replyToEvent={this.props.replyToEvent}
                     onChange={this.onChange}
                     disabled={this.state.haveRecording}
diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx
index 205320fb68..aca397b6b2 100644
--- a/src/components/views/rooms/SendMessageComposer.tsx
+++ b/src/components/views/rooms/SendMessageComposer.tsx
@@ -57,15 +57,16 @@ import { ActionPayload } from "../../../dispatcher/payloads";
 
 function addReplyToMessageContent(
     content: IContent,
-    repliedToEvent: MatrixEvent,
+    replyToEvent: MatrixEvent,
+    replyInThread: boolean,
     permalinkCreator: RoomPermalinkCreator,
 ): void {
-    const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
+    const replyContent = ReplyThread.makeReplyMixIn(replyToEvent, replyInThread);
     Object.assign(content, replyContent);
 
     // Part of Replies fallback support - prepend the text we're sending
     // with the text we're replying to
-    const nestedReply = ReplyThread.getNestedReplyText(repliedToEvent, permalinkCreator);
+    const nestedReply = ReplyThread.getNestedReplyText(replyToEvent, permalinkCreator);
     if (nestedReply) {
         if (content.formatted_body) {
             content.formatted_body = nestedReply.html + content.formatted_body;
@@ -77,8 +78,9 @@ function addReplyToMessageContent(
 // exported for tests
 export function createMessageContent(
     model: EditorModel,
-    permalinkCreator: RoomPermalinkCreator,
     replyToEvent: MatrixEvent,
+    replyInThread: boolean,
+    permalinkCreator: RoomPermalinkCreator,
 ): IContent {
     const isEmote = containsEmote(model);
     if (isEmote) {
@@ -101,7 +103,7 @@ export function createMessageContent(
     }
 
     if (replyToEvent) {
-        addReplyToMessageContent(content, replyToEvent, permalinkCreator);
+        addReplyToMessageContent(content, replyToEvent, replyInThread, permalinkCreator);
     }
 
     return content;
@@ -129,6 +131,7 @@ interface IProps {
     room: Room;
     placeholder?: string;
     permalinkCreator: RoomPermalinkCreator;
+    replyInThread?: boolean;
     replyToEvent?: MatrixEvent;
     disabled?: boolean;
     onChange?(model: EditorModel): void;
@@ -357,7 +360,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
                 if (cmd.category === CommandCategories.messages) {
                     content = await this.runSlashCommand(cmd, args);
                     if (replyToEvent) {
-                        addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
+                        addReplyToMessageContent(
+                            content,
+                            replyToEvent,
+                            this.props.replyInThread,
+                            this.props.permalinkCreator,
+                        );
                     }
                 } else {
                     this.runSlashCommand(cmd, args);
@@ -400,7 +408,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
             const startTime = CountlyAnalytics.getTimestamp();
             const { roomId } = this.props.room;
             if (!content) {
-                content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
+                content = createMessageContent(
+                    this.model,
+                    replyToEvent,
+                    this.props.replyInThread,
+                    this.props.permalinkCreator,
+                );
             }
             // don't bother sending an empty message
             if (!content.body.trim()) return;
diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js
index 0c4bde76a8..db5b55df90 100644
--- a/test/components/views/rooms/SendMessageComposer-test.js
+++ b/test/components/views/rooms/SendMessageComposer-test.js
@@ -46,7 +46,7 @@ describe('<SendMessageComposer/>', () => {
             const model = new EditorModel([], createPartCreator(), createRenderer());
             model.update("hello world", "insertText", { offset: 11, atNodeEnd: true });
 
-            const content = createMessageContent(model, permalinkCreator);
+            const content = createMessageContent(model, null, false, permalinkCreator);
 
             expect(content).toEqual({
                 body: "hello world",
@@ -58,7 +58,7 @@ describe('<SendMessageComposer/>', () => {
             const model = new EditorModel([], createPartCreator(), createRenderer());
             model.update("hello *world*", "insertText", { offset: 13, atNodeEnd: true });
 
-            const content = createMessageContent(model, permalinkCreator);
+            const content = createMessageContent(model, null, false, permalinkCreator);
 
             expect(content).toEqual({
                 body: "hello *world*",
@@ -72,7 +72,7 @@ describe('<SendMessageComposer/>', () => {
             const model = new EditorModel([], createPartCreator(), createRenderer());
             model.update("/me blinks __quickly__", "insertText", { offset: 22, atNodeEnd: true });
 
-            const content = createMessageContent(model, permalinkCreator);
+            const content = createMessageContent(model, null, false, permalinkCreator);
 
             expect(content).toEqual({
                 body: "blinks __quickly__",
@@ -86,7 +86,7 @@ describe('<SendMessageComposer/>', () => {
             const model = new EditorModel([], createPartCreator(), createRenderer());
             model.update("//dev/null is my favourite place", "insertText", { offset: 32, atNodeEnd: true });
 
-            const content = createMessageContent(model, permalinkCreator);
+            const content = createMessageContent(model, null, false, permalinkCreator);
 
             expect(content).toEqual({
                 body: "/dev/null is my favourite place",

From 2ce86471206cba5f54985e63d9f9a16c91cc6d59 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germains@element.io>
Date: Thu, 2 Sep 2021 08:36:20 +0100
Subject: [PATCH 3/3] Prevent unstable property to be sent with all events

---
 src/components/views/elements/ReplyThread.tsx | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx
index d5b6af17f2..d061d52f46 100644
--- a/src/components/views/elements/ReplyThread.tsx
+++ b/src/components/views/elements/ReplyThread.tsx
@@ -209,14 +209,26 @@ export default class ReplyThread extends React.Component<IProps, IState> {
 
     public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) {
         if (!ev) return {};
-        return {
+
+        const replyMixin = {
             'm.relates_to': {
                 'm.in_reply_to': {
                     'event_id': ev.getId(),
-                    [UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]: replyInThread,
                 },
             },
         };
+
+        /**
+         * @experimental
+         * Rendering hint for threads, only attached if true to make
+         * sure that Element does not start sending that property for all events
+         */
+        if (replyInThread) {
+            const inReplyTo = replyMixin['m.relates_to']['m.in_reply_to'];
+            inReplyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = replyInThread;
+        }
+
+        return replyMixin;
     }
 
     public static makeThread(