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(