From 02939fee0f7815fd653f82b353f5b27465383649 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Mon, 3 Apr 2023 14:48:32 +0100
Subject: [PATCH 01/31] pass handleCommand prop down and use it in
 WysiwygAutocomplete

---
 .../components/WysiwygAutocomplete.tsx          | 17 ++++++++++++++++-
 .../components/WysiwygComposer.tsx              |  9 +++++++--
 2 files changed, 23 insertions(+), 3 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
index 3afa409c717..0cff876aa1e 100644
--- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
@@ -35,6 +35,12 @@ interface WysiwygAutocompleteProps {
      * a mention in the autocomplete list or pressing enter on a selected item
      */
     handleMention: FormattingFunctions["mention"];
+
+    /**
+     * This handler will be called with the display text for a command on clicking
+     * a command in the autocomplete list or pressing enter on a selected item
+     */
+    handleCommand: FormattingFunctions["command"];
 }
 
 /**
@@ -45,13 +51,22 @@ interface WysiwygAutocompleteProps {
  * @param props.ref - the ref will be attached to the rendered `<Autocomplete />` component
  */
 const WysiwygAutocomplete = forwardRef(
-    ({ suggestion, handleMention }: WysiwygAutocompleteProps, ref: ForwardedRef<Autocomplete>): JSX.Element | null => {
+    (
+        { suggestion, handleMention, handleCommand }: WysiwygAutocompleteProps,
+        ref: ForwardedRef<Autocomplete>,
+    ): JSX.Element | null => {
         const { room } = useRoomContext();
         const client = useMatrixClientContext();
 
         function handleConfirm(completion: ICompletion): void {
             // TODO handle all of the completion types
             // Using this to pick out the ones we can handle during implementation
+            if (completion.type === "command") {
+                // trim the completion text as we add the trailing space in the rust model
+                // nb there are utils like parseCommandString and the CommandMap in SlashCommands.tsx
+                // that might be required here, but for now just use the trimmed completion text
+                handleCommand(completion.completion.trim());
+            }
             if (client && room && completion.href && (completion.type === "room" || completion.type === "user")) {
                 handleMention(
                     completion.href,
diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
index ca42ba22ec4..66c28ff4d5b 100644
--- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
@@ -91,7 +91,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
             }
         }
 
-        const mentions = ref.current?.querySelectorAll("a[data-mention-type]");
+        const mentions: NodeList = ref.current?.querySelectorAll("a[data-mention-type]");
         if (mentions) {
             mentions.forEach((mention) => mention.addEventListener("click", handleClick));
         }
@@ -108,7 +108,12 @@ export const WysiwygComposer = memo(function WysiwygComposer({
             onFocus={onFocus}
             onBlur={onFocus}
         >
-            <WysiwygAutocomplete ref={autocompleteRef} suggestion={suggestion} handleMention={wysiwyg.mention} />
+            <WysiwygAutocomplete
+                ref={autocompleteRef}
+                suggestion={suggestion}
+                handleMention={wysiwyg.mention}
+                handleCommand={wysiwyg.command}
+            />
             <FormattingButtons composer={wysiwyg} actionStates={actionStates} />
             <Editor
                 ref={ref}

From d4cadadb3efc88c8babfbda1bc9acb22de4b2554 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Mon, 3 Apr 2023 14:49:17 +0100
Subject: [PATCH 02/31] allow a command to generate a query from buildQuery

---
 .../views/rooms/wysiwyg_composer/utils/autocomplete.ts         | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts b/src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts
index d1f066a7bc4..86dd2791a71 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/autocomplete.ts
@@ -29,9 +29,8 @@ import * as Avatar from "../../../../../Avatar";
  * with @ for a user query, # for a room or space query
  */
 export function buildQuery(suggestion: MappedSuggestion | null): string {
-    if (!suggestion || !suggestion.keyChar || suggestion.type === "command") {
+    if (!suggestion || !suggestion.keyChar) {
         // if we have an empty key character, we do not build a query
-        // TODO implement the command functionality
         return "";
     }
 

From 4fe00774794677e8744a43f8a38b635c8c2a7301 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Mon, 3 Apr 2023 15:13:32 +0100
Subject: [PATCH 03/31] port command functionality into the sendMessage util

---
 .../rooms/wysiwyg_composer/utils/message.ts   | 60 ++++++++++++++++++-
 1 file changed, 58 insertions(+), 2 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 70b65e392e5..531616b962b 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
-import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { IContent, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
 import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
 
@@ -33,6 +33,10 @@ import { endEditing, cancelPreviousPendingEdit } from "./editing";
 import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
 import { createMessageContent } from "./createMessageContent";
 import { isContentModified } from "./isContentModified";
+import { CommandCategories, getCommand } from "../../../../../SlashCommands";
+import { runSlashCommand, shouldSendAnyway } from "../../../../../editor/commands";
+import { attachRelation } from "../../SendMessageComposer";
+import { Action } from "../../../../../dispatcher/actions";
 
 export interface SendMessageParams {
     mxClient: MatrixClient;
@@ -48,6 +52,7 @@ export async function sendMessage(
     isHTML: boolean,
     { roomContext, mxClient, ...params }: SendMessageParams,
 ): Promise<ISendEventResponse> {
+    console.log(`<<< sending ${message}`);
     const { relation, replyToEvent } = params;
     const { room } = roomContext;
     const roomId = room?.roomId;
@@ -71,7 +76,58 @@ export async function sendMessage(
     }*/
     PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
 
-    const content = await createMessageContent(message, isHTML, params);
+    let shouldSend = true;
+    let content: IContent | null = null;
+
+    // TODO slash command - quick and dirty to start
+    if (message[0] === "/") {
+        // then we have a slash command, let's use the existing functions as much as possible for processing
+        const { cmd, args } = getCommand(message);
+        if (cmd) {
+            // debugger; // we will need to handle /me separately, see
+            // /Users/alunturner/code/matrix-react-sdk/src/components/views/rooms/wysiwyg_composer/utils/message.tsx:87
+            const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
+            let commandSuccessful: boolean;
+            [content, commandSuccessful] = await runSlashCommand(cmd, args, roomId, threadId ?? null);
+
+            if (!commandSuccessful) {
+                return; // errored
+            }
+
+            if (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects) {
+                attachRelation(content, relation);
+                // if (replyToEvent) {
+                //     addReplyToMessageContent(content, replyToEvent, {
+                //         permalinkCreator: this.props.permalinkCreator,
+                //         // Exclude the legacy fallback for custom event types such as those used by /fireworks
+                //         includeLegacyFallback: content.msgtype?.startsWith("m.") ?? true,
+                //     });
+                // }
+            } else {
+                shouldSend = false;
+            }
+        } else {
+            const sendAnyway = await shouldSendAnyway(message);
+            // re-focus the composer after QuestionDialog is closed
+            dis.dispatch({
+                action: Action.FocusAComposer, // TODO this does not refocus the wysiwyg composer
+                context: roomContext.timelineRenderingType,
+            });
+            // if !sendAnyway bail to let the user edit the composer and try again
+            if (!sendAnyway) return;
+        }
+    }
+
+    // early return to save nesting the whole of the next bit, perhaps this could be handled more neatly
+    // in the if block above?
+    if (!shouldSend) {
+        return;
+    }
+
+    // we haven't done a slash command
+    if (!content) {
+        content = await createMessageContent(message, isHTML, params);
+    }
 
     // TODO slash comment
 

From 6b623d072a9bd138cd3e158774afef4772c49c80 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Mon, 3 Apr 2023 15:20:27 +0100
Subject: [PATCH 04/31] tidy up comments

---
 .../views/rooms/wysiwyg_composer/utils/message.ts         | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 531616b962b..d2a491c931e 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -79,13 +79,11 @@ export async function sendMessage(
     let shouldSend = true;
     let content: IContent | null = null;
 
-    // TODO slash command - quick and dirty to start
-    if (message[0] === "/") {
-        // then we have a slash command, let's use the existing functions as much as possible for processing
+    // Functionality here approximates what can be found in SendMessageComposer.sendMessage()
+    if (message.startsWith("/") && !message.startsWith("//")) {
         const { cmd, args } = getCommand(message);
         if (cmd) {
-            // debugger; // we will need to handle /me separately, see
-            // /Users/alunturner/code/matrix-react-sdk/src/components/views/rooms/wysiwyg_composer/utils/message.tsx:87
+            // we will need to handle /me separately, see SlashCommands.tsx:1387
             const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
             let commandSuccessful: boolean;
             [content, commandSuccessful] = await runSlashCommand(cmd, args, roomId, threadId ?? null);

From e056d5dad5d48ab46b1be7581fdbb6b6b4cb5415 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Mon, 3 Apr 2023 15:23:36 +0100
Subject: [PATCH 05/31] remove use of shouldSend and update comments

---
 .../views/rooms/wysiwyg_composer/utils/message.ts     | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index d2a491c931e..6a0226c2792 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -76,7 +76,6 @@ export async function sendMessage(
     }*/
     PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
 
-    let shouldSend = true;
     let content: IContent | null = null;
 
     // Functionality here approximates what can be found in SendMessageComposer.sendMessage()
@@ -94,6 +93,7 @@ export async function sendMessage(
 
             if (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects) {
                 attachRelation(content, relation);
+                // TODO translate this block for replies
                 // if (replyToEvent) {
                 //     addReplyToMessageContent(content, replyToEvent, {
                 //         permalinkCreator: this.props.permalinkCreator,
@@ -102,7 +102,8 @@ export async function sendMessage(
                 //     });
                 // }
             } else {
-                shouldSend = false;
+                // instead of setting shouldSend to false as in SendMessageComposer, just return
+                return;
             }
         } else {
             const sendAnyway = await shouldSendAnyway(message);
@@ -116,12 +117,6 @@ export async function sendMessage(
         }
     }
 
-    // early return to save nesting the whole of the next bit, perhaps this could be handled more neatly
-    // in the if block above?
-    if (!shouldSend) {
-        return;
-    }
-
     // we haven't done a slash command
     if (!content) {
         content = await createMessageContent(message, isHTML, params);

From c0668f4f254ac0fbfa6c011ae34213ee09c85151 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Mon, 3 Apr 2023 15:34:26 +0100
Subject: [PATCH 06/31] remove console log

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 6a0226c2792..6b7265e772b 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -52,7 +52,6 @@ export async function sendMessage(
     isHTML: boolean,
     { roomContext, mxClient, ...params }: SendMessageParams,
 ): Promise<ISendEventResponse> {
-    console.log(`<<< sending ${message}`);
     const { relation, replyToEvent } = params;
     const { room } = roomContext;
     const roomId = room?.roomId;

From 212a9988c6de6351b8792c72d7047556aa926ad8 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Wed, 5 Apr 2023 10:32:03 +0100
Subject: [PATCH 07/31] make logic more explicit and amend comment

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 6b7265e772b..1e6989b3592 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -116,8 +116,8 @@ export async function sendMessage(
         }
     }
 
-    // we haven't done a slash command
-    if (!content) {
+    // if content is still null, we haven't done any slash command processing so generate some content
+    if (content === null) {
         content = await createMessageContent(message, isHTML, params);
     }
 

From c65a1b74b40d0c055032a1065f7c35718a8c0ad7 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Wed, 5 Apr 2023 10:40:38 +0100
Subject: [PATCH 08/31] uncomment replyToEvent block

---
 .../rooms/wysiwyg_composer/utils/message.ts    | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 1e6989b3592..5ad23a99dbc 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -37,6 +37,7 @@ import { CommandCategories, getCommand } from "../../../../../SlashCommands";
 import { runSlashCommand, shouldSendAnyway } from "../../../../../editor/commands";
 import { attachRelation } from "../../SendMessageComposer";
 import { Action } from "../../../../../dispatcher/actions";
+import { addReplyToMessageContent } from "../../../../../utils/Reply";
 
 export interface SendMessageParams {
     mxClient: MatrixClient;
@@ -52,7 +53,7 @@ export async function sendMessage(
     isHTML: boolean,
     { roomContext, mxClient, ...params }: SendMessageParams,
 ): Promise<ISendEventResponse> {
-    const { relation, replyToEvent } = params;
+    const { relation, replyToEvent, permalinkCreator } = params;
     const { room } = roomContext;
     const roomId = room?.roomId;
 
@@ -92,14 +93,13 @@ export async function sendMessage(
 
             if (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects) {
                 attachRelation(content, relation);
-                // TODO translate this block for replies
-                // if (replyToEvent) {
-                //     addReplyToMessageContent(content, replyToEvent, {
-                //         permalinkCreator: this.props.permalinkCreator,
-                //         // Exclude the legacy fallback for custom event types such as those used by /fireworks
-                //         includeLegacyFallback: content.msgtype?.startsWith("m.") ?? true,
-                //     });
-                // }
+                if (replyToEvent) {
+                    addReplyToMessageContent(content, replyToEvent, {
+                        permalinkCreator,
+                        // Exclude the legacy fallback for custom event types such as those used by /fireworks
+                        includeLegacyFallback: content.msgtype?.startsWith("m.") ?? true,
+                    });
+                }
             } else {
                 // instead of setting shouldSend to false as in SendMessageComposer, just return
                 return;

From f4c272d040427290812e6b7ed6ee9ad80d79d8be Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Wed, 5 Apr 2023 14:30:10 +0100
Subject: [PATCH 09/31] update util test

---
 .../wysiwyg_composer/utils/autocomplete-test.ts     | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/test/components/views/rooms/wysiwyg_composer/utils/autocomplete-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/autocomplete-test.ts
index 2612f037f2d..32923245acc 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/autocomplete-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/autocomplete-test.ts
@@ -53,15 +53,18 @@ describe("buildQuery", () => {
         expect(buildQuery(noKeyCharSuggestion)).toBe("");
     });
 
-    it("returns an empty string when suggestion is a command", () => {
-        // TODO alter this test when commands are implemented
-        const commandSuggestion = { keyChar: "/" as const, text: "slash", type: "command" as const };
-        expect(buildQuery(commandSuggestion)).toBe("");
-    });
+    // it("returns an empty string when suggestion is a command", () => {
+    //     // TODO alter this test when commands are implemented
+    //     const commandSuggestion = { keyChar: "/" as const, text: "slash", type: "command" as const };
+    //     expect(buildQuery(commandSuggestion)).toBe("");
+    // });
 
     it("combines the keyChar and text of the suggestion in the query", () => {
         const handledSuggestion = { keyChar: "@" as const, text: "alice", type: "mention" as const };
         expect(buildQuery(handledSuggestion)).toBe("@alice");
+
+        const handledCommand = { keyChar: "/" as const, text: "spoiler", type: "mention" as const };
+        expect(buildQuery(handledCommand)).toBe("/spoiler");
     });
 });
 

From 6f46bf8349a36276088586a14f99e0b1ad409ccd Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Wed, 5 Apr 2023 14:33:01 +0100
Subject: [PATCH 10/31] remove commented out test

---
 .../views/rooms/wysiwyg_composer/utils/autocomplete-test.ts | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/test/components/views/rooms/wysiwyg_composer/utils/autocomplete-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/autocomplete-test.ts
index 32923245acc..dc89caba65e 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/autocomplete-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/autocomplete-test.ts
@@ -53,12 +53,6 @@ describe("buildQuery", () => {
         expect(buildQuery(noKeyCharSuggestion)).toBe("");
     });
 
-    // it("returns an empty string when suggestion is a command", () => {
-    //     // TODO alter this test when commands are implemented
-    //     const commandSuggestion = { keyChar: "/" as const, text: "slash", type: "command" as const };
-    //     expect(buildQuery(commandSuggestion)).toBe("");
-    // });
-
     it("combines the keyChar and text of the suggestion in the query", () => {
         const handledSuggestion = { keyChar: "@" as const, text: "alice", type: "mention" as const };
         expect(buildQuery(handledSuggestion)).toBe("@alice");

From 0db94cb79cc74eafd3ad06754438412b505fbb6d Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Wed, 5 Apr 2023 17:01:21 +0100
Subject: [PATCH 11/31] use local text over import from current composer

---
 .../views/rooms/wysiwyg_composer/utils/message.ts      | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 5ad23a99dbc..a8f27c25fbf 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -35,10 +35,18 @@ import { createMessageContent } from "./createMessageContent";
 import { isContentModified } from "./isContentModified";
 import { CommandCategories, getCommand } from "../../../../../SlashCommands";
 import { runSlashCommand, shouldSendAnyway } from "../../../../../editor/commands";
-import { attachRelation } from "../../SendMessageComposer";
 import { Action } from "../../../../../dispatcher/actions";
 import { addReplyToMessageContent } from "../../../../../utils/Reply";
 
+// Merges favouring the given relation - taken from SendMessageComposer to avoid another import
+function attachRelation(content: IContent, relation?: IEventRelation): void {
+    if (relation) {
+        content["m.relates_to"] = {
+            ...(content["m.relates_to"] || {}),
+            ...relation,
+        };
+    }
+}
 export interface SendMessageParams {
     mxClient: MatrixClient;
     relation?: IEventRelation;

From f8bddc06106a216f63384f565ebe0f31ce24a569 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Wed, 5 Apr 2023 17:01:30 +0100
Subject: [PATCH 12/31] expand tests

---
 .../wysiwyg_composer/utils/message-test.ts    | 95 ++++++++++++++++++-
 1 file changed, 94 insertions(+), 1 deletion(-)

diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
index 49442127d05..a2452173246 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { EventStatus } from "matrix-js-sdk/src/matrix";
+import { EventStatus, IEventRelation } from "matrix-js-sdk/src/matrix";
 
 import { IRoomState } from "../../../../../../src/components/structures/RoomView";
 import { editMessage, sendMessage } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/message";
@@ -25,6 +25,8 @@ import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
 import { RoomPermalinkCreator } from "../../../../../../src/utils/permalinks/Permalinks";
 import EditorStateTransfer from "../../../../../../src/utils/EditorStateTransfer";
 import * as ConfirmRedactDialog from "../../../../../../src/components/views/dialogs/ConfirmRedactDialog";
+import * as SlashCommands from "../../../../../../src/SlashCommands";
+import * as Commands from "../../../../../../src/editor/commands";
 
 describe("message", () => {
     const permalinkCreator = {
@@ -226,6 +228,97 @@ describe("message", () => {
             // Then
             expect(spyDispatcher).toHaveBeenCalledWith({ action: "effects.confetti" });
         });
+
+        describe.only("slash commands", () => {
+            afterEach(() => {
+                jest.restoreAllMocks();
+            });
+
+            it("calls getCommand for a message starting with a valid command", async () => {
+                // When
+                const validCommand = "/spoiler";
+                const getCommandSpy = jest.spyOn(SlashCommands, "getCommand");
+                await sendMessage(validCommand, true, {
+                    roomContext: defaultRoomContext,
+                    mxClient: mockClient,
+                    permalinkCreator,
+                });
+
+                // Then
+                expect(getCommandSpy).toHaveBeenCalledWith(validCommand);
+            });
+
+            it("does not call getCommand for valid command with invalid prefix", async () => {
+                // When
+                const invalidPrefixCommand = "//spoiler";
+                const getCommandSpy = jest.spyOn(SlashCommands, "getCommand");
+                await sendMessage(invalidPrefixCommand, true, {
+                    roomContext: defaultRoomContext,
+                    mxClient: mockClient,
+                    permalinkCreator,
+                });
+
+                // Then
+                expect(getCommandSpy).toHaveBeenCalledTimes(0);
+            });
+
+            // TODO amend test when TS fixes are made - this currently can't actually return undefined
+            // according to the TS types
+            it("returns undefined when the command is not successful", async () => {
+                // When
+                const validCommand = "/spoiler";
+                jest.spyOn(Commands, "runSlashCommand").mockResolvedValue([{ content: "mock content" }, false]);
+
+                const result = await sendMessage(validCommand, true, {
+                    roomContext: defaultRoomContext,
+                    mxClient: mockClient,
+                    permalinkCreator,
+                });
+
+                // Then
+                expect(result).toBeUndefined();
+            });
+
+            // /spoiler is a .messages category command, /fireworks is an .effect category command
+            const messagesAndEffectCategoryTestCases = ["/spoiler text", "/fireworks"];
+
+            it.each(messagesAndEffectCategoryTestCases)(
+                "does not add relations for a .messages or .effects category command if there is no relation to add",
+                async (inputText) => {
+                    await sendMessage(inputText, true, {
+                        roomContext: defaultRoomContext,
+                        mxClient: mockClient,
+                        permalinkCreator,
+                    });
+                    expect(mockClient.sendMessage).toHaveBeenCalledWith(
+                        "myfakeroom",
+                        null,
+                        expect.not.objectContaining({ "m.relates_to": expect.any }),
+                    );
+                },
+            );
+
+            it.each(messagesAndEffectCategoryTestCases)(
+                "adds relations for a .messages or .effects category command if there is a relation",
+                async (inputText) => {
+                    const mockRelation: IEventRelation = {
+                        rel_type: "mock relation type",
+                    };
+                    await sendMessage(inputText, true, {
+                        roomContext: defaultRoomContext,
+                        mxClient: mockClient,
+                        permalinkCreator,
+                        relation: mockRelation,
+                    });
+
+                    expect(mockClient.sendMessage).toHaveBeenCalledWith(
+                        "myfakeroom",
+                        null,
+                        expect.objectContaining({ "m.relates_to": expect.objectContaining(mockRelation) }),
+                    );
+                },
+            );
+        });
     });
 
     describe("editMessage", () => {

From 735e8a8edd20e8fb897f324232c333afc9d4a891 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 10:14:17 +0100
Subject: [PATCH 13/31] expand tests

---
 .../wysiwyg_composer/utils/message-test.ts    | 68 +++++++++++++++++++
 1 file changed, 68 insertions(+)

diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
index a2452173246..7fe16e44a23 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
@@ -27,6 +27,8 @@ import EditorStateTransfer from "../../../../../../src/utils/EditorStateTransfer
 import * as ConfirmRedactDialog from "../../../../../../src/components/views/dialogs/ConfirmRedactDialog";
 import * as SlashCommands from "../../../../../../src/SlashCommands";
 import * as Commands from "../../../../../../src/editor/commands";
+import * as Reply from "../../../../../../src/utils/Reply";
+import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
 
 describe("message", () => {
     const permalinkCreator = {
@@ -49,6 +51,9 @@ describe("message", () => {
     });
 
     const mockClient = createTestClient();
+    mockClient.setDisplayName = jest.fn().mockResolvedValue({});
+    mockClient.setRoomName = jest.fn().mockResolvedValue({});
+
     const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any;
     mockRoom.findEventById = jest.fn((eventId) => {
         return eventId === mockEvent.getId() ? mockEvent : null;
@@ -58,6 +63,9 @@ describe("message", () => {
 
     const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch");
 
+    beforeEach(() => {
+        jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
+    });
     afterEach(() => {
         jest.resetAllMocks();
     });
@@ -281,6 +289,7 @@ describe("message", () => {
 
             // /spoiler is a .messages category command, /fireworks is an .effect category command
             const messagesAndEffectCategoryTestCases = ["/spoiler text", "/fireworks"];
+            const otherCategoryTestCases = ["/nick new_nickname", "/roomname new_room_name"];
 
             it.each(messagesAndEffectCategoryTestCases)(
                 "does not add relations for a .messages or .effects category command if there is no relation to add",
@@ -318,6 +327,65 @@ describe("message", () => {
                     );
                 },
             );
+
+            it("calls addReplyToMessageContent when there is an event to reply to", async () => {
+                const addReplySpy = jest.spyOn(Reply, "addReplyToMessageContent");
+                await sendMessage("input", true, {
+                    roomContext: defaultRoomContext,
+                    mxClient: mockClient,
+                    permalinkCreator,
+                    replyToEvent: mockEvent,
+                });
+
+                expect(addReplySpy).toHaveBeenCalledTimes(1);
+            });
+
+            // TODO - type will change here when I fix the TS errors (maybe)
+            it.each(otherCategoryTestCases)(
+                "returns undefined when the command category is not .messages or .effects",
+                async (input) => {
+                    const result = await sendMessage(input, true, {
+                        roomContext: defaultRoomContext,
+                        mxClient: mockClient,
+                        permalinkCreator,
+                        replyToEvent: mockEvent,
+                    });
+
+                    expect(result).toBeUndefined();
+                },
+            );
+
+            it("if user enters invalid command and then sends it anyway, message is sent", async () => {
+                // mock out returning a true value for `shouldSendAnyway` to avoid rendering the modal
+                jest.spyOn(Commands, "shouldSendAnyway").mockResolvedValue(true);
+                const invalidCommandInput = "/badCommand";
+
+                await sendMessage(invalidCommandInput, true, {
+                    roomContext: defaultRoomContext,
+                    mxClient: mockClient,
+                    permalinkCreator,
+                });
+
+                expect(mockClient.sendMessage).toHaveBeenCalledWith(
+                    "myfakeroom",
+                    null,
+                    expect.objectContaining({ body: invalidCommandInput }),
+                );
+            });
+
+            it("if user enters invalid command and then does not send, return undefined", async () => {
+                // mock out returning a true value for `shouldSendAnyway` to avoid rendering the modal
+                jest.spyOn(Commands, "shouldSendAnyway").mockResolvedValue(false);
+                const invalidCommandInput = "/badCommand";
+
+                const result = await sendMessage(invalidCommandInput, true, {
+                    roomContext: defaultRoomContext,
+                    mxClient: mockClient,
+                    permalinkCreator,
+                });
+
+                expect(result).toBeUndefined();
+            });
         });
     });
 

From b0c62a3e495c5a4687e5aa005147855350c0377b Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 10:26:09 +0100
Subject: [PATCH 14/31] handle the FocusAComposer action for the wysiwyg
 composer

---
 .../rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts
index 40b7e8182cb..50a229b6371 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts
@@ -46,6 +46,7 @@ export function useWysiwygSendActionHandler(
 
             switch (payload.action) {
                 case "reply_to_event":
+                case Action.FocusAComposer:
                 case Action.FocusSendMessageComposer:
                     focusComposer(composerElement, context, roomContext, timeoutId);
                     break;

From 86347a48ceabaa89b38aab53b7dc0ed118688a6a Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 10:26:16 +0100
Subject: [PATCH 15/31] remove TODO comment

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index a8f27c25fbf..a8f0ee74c84 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -116,7 +116,7 @@ export async function sendMessage(
             const sendAnyway = await shouldSendAnyway(message);
             // re-focus the composer after QuestionDialog is closed
             dis.dispatch({
-                action: Action.FocusAComposer, // TODO this does not refocus the wysiwyg composer
+                action: Action.FocusAComposer,
                 context: roomContext.timelineRenderingType,
             });
             // if !sendAnyway bail to let the user edit the composer and try again

From 0282c1416d4a17f6d44fa172a901644db7b02ec8 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 10:29:01 +0100
Subject: [PATCH 16/31] remove TODO

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index a8f0ee74c84..02ac9777a63 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -129,8 +129,6 @@ export async function sendMessage(
         content = await createMessageContent(message, isHTML, params);
     }
 
-    // TODO slash comment
-
     // TODO replace emotion end of message ?
 
     // TODO quick reaction

From 377e191352d963a6615ebc71bcaba4f0545f20a4 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 10:29:17 +0100
Subject: [PATCH 17/31] test for action dispatch

---
 .../views/rooms/wysiwyg_composer/utils/message-test.ts      | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
index 7fe16e44a23..d37ca8bc1f3 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
@@ -29,6 +29,7 @@ import * as SlashCommands from "../../../../../../src/SlashCommands";
 import * as Commands from "../../../../../../src/editor/commands";
 import * as Reply from "../../../../../../src/utils/Reply";
 import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
+import { Action } from "../../../../../../src/dispatcher/actions";
 
 describe("message", () => {
     const permalinkCreator = {
@@ -355,7 +356,7 @@ describe("message", () => {
                 },
             );
 
-            it("if user enters invalid command and then sends it anyway, message is sent", async () => {
+            it("if user enters invalid command and then sends it anyway", async () => {
                 // mock out returning a true value for `shouldSendAnyway` to avoid rendering the modal
                 jest.spyOn(Commands, "shouldSendAnyway").mockResolvedValue(true);
                 const invalidCommandInput = "/badCommand";
@@ -366,11 +367,14 @@ describe("message", () => {
                     permalinkCreator,
                 });
 
+                // we expect the message to have been sent
+                // and a composer focus action to have been dispatched
                 expect(mockClient.sendMessage).toHaveBeenCalledWith(
                     "myfakeroom",
                     null,
                     expect.objectContaining({ body: invalidCommandInput }),
                 );
+                expect(spyDispatcher).toHaveBeenCalledWith(expect.objectContaining({ action: Action.FocusAComposer }));
             });
 
             it("if user enters invalid command and then does not send, return undefined", async () => {

From 66f8380f5d37f81f86e8e4be7c4f9615d9242cd7 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 11:33:05 +0100
Subject: [PATCH 18/31] fix failing tests

---
 .../wysiwyg_composer/utils/message-test.ts    | 20 +++++++++----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
index d37ca8bc1f3..eb7127ad198 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
@@ -66,9 +66,11 @@ describe("message", () => {
 
     beforeEach(() => {
         jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
+        jest.clearAllMocks();
     });
-    afterEach(() => {
-        jest.resetAllMocks();
+
+    afterAll(() => {
+        jest.restoreAllMocks();
     });
 
     describe("sendMessage", () => {
@@ -238,15 +240,12 @@ describe("message", () => {
             expect(spyDispatcher).toHaveBeenCalledWith({ action: "effects.confetti" });
         });
 
-        describe.only("slash commands", () => {
-            afterEach(() => {
-                jest.restoreAllMocks();
-            });
+        describe("slash commands", () => {
+            const getCommandSpy = jest.spyOn(SlashCommands, "getCommand");
 
             it("calls getCommand for a message starting with a valid command", async () => {
                 // When
                 const validCommand = "/spoiler";
-                const getCommandSpy = jest.spyOn(SlashCommands, "getCommand");
                 await sendMessage(validCommand, true, {
                     roomContext: defaultRoomContext,
                     mxClient: mockClient,
@@ -260,7 +259,6 @@ describe("message", () => {
             it("does not call getCommand for valid command with invalid prefix", async () => {
                 // When
                 const invalidPrefixCommand = "//spoiler";
-                const getCommandSpy = jest.spyOn(SlashCommands, "getCommand");
                 await sendMessage(invalidPrefixCommand, true, {
                     roomContext: defaultRoomContext,
                     mxClient: mockClient,
@@ -276,7 +274,7 @@ describe("message", () => {
             it("returns undefined when the command is not successful", async () => {
                 // When
                 const validCommand = "/spoiler";
-                jest.spyOn(Commands, "runSlashCommand").mockResolvedValue([{ content: "mock content" }, false]);
+                jest.spyOn(Commands, "runSlashCommand").mockResolvedValueOnce([{ content: "mock content" }, false]);
 
                 const result = await sendMessage(validCommand, true, {
                     roomContext: defaultRoomContext,
@@ -358,7 +356,7 @@ describe("message", () => {
 
             it("if user enters invalid command and then sends it anyway", async () => {
                 // mock out returning a true value for `shouldSendAnyway` to avoid rendering the modal
-                jest.spyOn(Commands, "shouldSendAnyway").mockResolvedValue(true);
+                jest.spyOn(Commands, "shouldSendAnyway").mockResolvedValueOnce(true);
                 const invalidCommandInput = "/badCommand";
 
                 await sendMessage(invalidCommandInput, true, {
@@ -379,7 +377,7 @@ describe("message", () => {
 
             it("if user enters invalid command and then does not send, return undefined", async () => {
                 // mock out returning a true value for `shouldSendAnyway` to avoid rendering the modal
-                jest.spyOn(Commands, "shouldSendAnyway").mockResolvedValue(false);
+                jest.spyOn(Commands, "shouldSendAnyway").mockResolvedValueOnce(false);
                 const invalidCommandInput = "/badCommand";
 
                 const result = await sendMessage(invalidCommandInput, true, {

From 8cddb2518efcdc2ddb023edc4f3e7f6f053eefe5 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 11:38:38 +0100
Subject: [PATCH 19/31] tidy up tests

---
 .../views/rooms/wysiwyg_composer/utils/message-test.ts       | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
index eb7127ad198..8a9dd1617d0 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
@@ -288,8 +288,6 @@ describe("message", () => {
 
             // /spoiler is a .messages category command, /fireworks is an .effect category command
             const messagesAndEffectCategoryTestCases = ["/spoiler text", "/fireworks"];
-            const otherCategoryTestCases = ["/nick new_nickname", "/roomname new_room_name"];
-
             it.each(messagesAndEffectCategoryTestCases)(
                 "does not add relations for a .messages or .effects category command if there is no relation to add",
                 async (inputText) => {
@@ -340,6 +338,9 @@ describe("message", () => {
             });
 
             // TODO - type will change here when I fix the TS errors (maybe)
+
+            // these test cases are .action and .admin categories
+            const otherCategoryTestCases = ["/nick new_nickname", "/roomname new_room_name"];
             it.each(otherCategoryTestCases)(
                 "returns undefined when the command category is not .messages or .effects",
                 async (input) => {

From 000d74480d68f7422f71aef619d453f7e57401e9 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 11:48:42 +0100
Subject: [PATCH 20/31] fix TS error and improve typing

---
 .../components/WysiwygAutocomplete-test.tsx         | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx
index e4de34c2691..b666ff1ff9f 100644
--- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx
@@ -69,8 +69,9 @@ describe("WysiwygAutocomplete", () => {
         },
     ]);
     const mockHandleMention = jest.fn();
+    const mockHandleCommand = jest.fn();
 
-    const renderComponent = (props = {}) => {
+    const renderComponent = (props: Partial<React.ComponentProps<typeof WysiwygAutocomplete>> = {}) => {
         const mockClient = stubClient();
         const mockRoom = mkStubRoom("test_room", "test_room", mockClient);
         const mockRoomContext = getRoomContext(mockRoom, {});
@@ -82,6 +83,7 @@ describe("WysiwygAutocomplete", () => {
                         ref={autocompleteRef}
                         suggestion={null}
                         handleMention={mockHandleMention}
+                        handleCommand={mockHandleCommand}
                         {...props}
                     />
                 </RoomContext.Provider>
@@ -90,7 +92,14 @@ describe("WysiwygAutocomplete", () => {
     };
 
     it("does not show the autocomplete when room is undefined", () => {
-        render(<WysiwygAutocomplete ref={autocompleteRef} suggestion={null} handleMention={mockHandleMention} />);
+        render(
+            <WysiwygAutocomplete
+                ref={autocompleteRef}
+                suggestion={null}
+                handleMention={mockHandleMention}
+                handleCommand={mockHandleCommand}
+            />,
+        );
         expect(screen.queryByTestId("autocomplete-wrapper")).not.toBeInTheDocument();
     });
 

From b1ca9999649bbb20eb2189fa3b9ee90ee65c89e8 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 12:14:16 +0100
Subject: [PATCH 21/31] fix TS error

---
 .../views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
index 66c28ff4d5b..56f79c94a87 100644
--- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx
@@ -91,7 +91,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
             }
         }
 
-        const mentions: NodeList = ref.current?.querySelectorAll("a[data-mention-type]");
+        const mentions: NodeList | undefined = ref.current?.querySelectorAll("a[data-mention-type]");
         if (mentions) {
             mentions.forEach((mention) => mention.addEventListener("click", handleClick));
         }

From 4abd905aad2b5a83d25884a31f1c8ad269bbda39 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 12:18:02 +0100
Subject: [PATCH 22/31] amend return types for sendMessage, editMessage

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 02ac9777a63..b075a97e084 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -60,7 +60,7 @@ export async function sendMessage(
     message: string,
     isHTML: boolean,
     { roomContext, mxClient, ...params }: SendMessageParams,
-): Promise<ISendEventResponse> {
+): Promise<ISendEventResponse | undefined> {
     const { relation, replyToEvent, permalinkCreator } = params;
     const { room } = roomContext;
     const roomId = room?.roomId;
@@ -200,7 +200,7 @@ interface EditMessageParams {
 export async function editMessage(
     html: string,
     { roomContext, mxClient, editorStateTransfer }: EditMessageParams,
-): Promise<ISendEventResponse> {
+): Promise<ISendEventResponse | undefined> {
     const editedEvent = editorStateTransfer.getEvent();
 
     PosthogAnalytics.instance.trackEvent<ComposerEvent>({

From fa13702a54e481b8eb68fcd1b1759228ef81ec43 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 12:20:54 +0100
Subject: [PATCH 23/31] fix null content TS error

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index b075a97e084..a0b80395b46 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -99,7 +99,10 @@ export async function sendMessage(
                 return; // errored
             }
 
-            if (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects) {
+            if (
+                content &&
+                (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects)
+            ) {
                 attachRelation(content, relation);
                 if (replyToEvent) {
                     addReplyToMessageContent(content, replyToEvent, {

From 01fa6a7ef8f5174fba51dadf0d1303619f4f4090 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 12:22:01 +0100
Subject: [PATCH 24/31] fix another null content TS error

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index a0b80395b46..a0563c7d517 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -165,7 +165,7 @@ export async function sendMessage(
 
     dis.dispatch({ action: "message_sent" });
     CHAT_EFFECTS.forEach((effect) => {
-        if (containsEmoji(content, effect.emojis)) {
+        if (content && containsEmoji(content, effect.emojis)) {
             // For initial threads launch, chat effects are disabled
             // see #19731
             const isNotThread = relation?.rel_type !== THREAD_RELATION_TYPE.name;

From cd4ce1bc3b3f924939745050b60104ef60bec55d Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 13:09:45 +0100
Subject: [PATCH 25/31] use as to correct final TS error

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index a0563c7d517..e3a6bfb6489 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -149,7 +149,9 @@ export async function sendMessage(
 
     const prom = doMaybeLocalRoomAction(
         roomId,
-        (actualRoomId: string) => mxClient.sendMessage(actualRoomId, threadId, content),
+        (actualRoomId: string) => {
+            return mxClient.sendMessage(actualRoomId, threadId, content as IContent, undefined);
+        },
         mxClient,
     );
 

From 45cc670a19c5b842500afa1725257c20d3865a88 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 13:31:35 +0100
Subject: [PATCH 26/31] remove undefined argument

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index e3a6bfb6489..587ce38f3fa 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -150,7 +150,7 @@ export async function sendMessage(
     const prom = doMaybeLocalRoomAction(
         roomId,
         (actualRoomId: string) => {
-            return mxClient.sendMessage(actualRoomId, threadId, content as IContent, undefined);
+            return mxClient.sendMessage(actualRoomId, threadId, content as IContent);
         },
         mxClient,
     );

From b77025b8195acc334a955b889899cd4d536ac4cb Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 13:32:23 +0100
Subject: [PATCH 27/31] try to fix TS errors for editMessage function usage

---
 .../DynamicImportWysiwygComposer.tsx                |  2 +-
 .../rooms/wysiwyg_composer/hooks/useEditing.ts      | 13 +++++++------
 2 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx
index 9df04f90f34..afe8396bbd0 100644
--- a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx
@@ -28,7 +28,7 @@ export const dynamicImportSendMessage = async (
     message: string,
     isHTML: boolean,
     params: SendMessageParams,
-): Promise<ISendEventResponse> => {
+): Promise<ISendEventResponse | undefined> => {
     const { sendMessage } = await import("./utils/message");
 
     return sendMessage(message, isHTML, params);
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts
index 862b7495026..ffb354dda7c 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts
@@ -29,7 +29,7 @@ export function useEditing(
 ): {
     isSaveDisabled: boolean;
     onChange(content: string): void;
-    editMessage(): Promise<ISendEventResponse>;
+    editMessage(): Promise<ISendEventResponse | undefined>;
     endEditing(): void;
 } {
     const roomContext = useRoomContext();
@@ -45,11 +45,12 @@ export function useEditing(
         [initialContent],
     );
 
-    const editMessageMemoized = useCallback(
-        () =>
-            !!mxClient && content !== undefined && editMessage(content, { roomContext, mxClient, editorStateTransfer }),
-        [content, roomContext, mxClient, editorStateTransfer],
-    );
+    const editMessageMemoized = useCallback(() => {
+        if (mxClient === undefined || content === undefined) {
+            return Promise.resolve(undefined);
+        }
+        return editMessage(content, { roomContext, mxClient, editorStateTransfer });
+    }, [content, roomContext, mxClient, editorStateTransfer]);
 
     const endEditingMemoized = useCallback(() => endEditing(roomContext), [roomContext]);
     return { onChange, editMessage: editMessageMemoized, endEditing: endEditingMemoized, isSaveDisabled };

From 2863b3ddc9e320394e2a74e06361acf3db671b1e Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 13:58:26 +0100
Subject: [PATCH 28/31] tidy up

---
 .../components/WysiwygAutocomplete.tsx        |  7 ++++---
 .../wysiwyg_composer/hooks/useEditing.ts      |  4 ++--
 .../rooms/wysiwyg_composer/utils/message.ts   | 20 ++++---------------
 3 files changed, 10 insertions(+), 21 deletions(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
index 0cff876aa1e..20e41fe0b12 100644
--- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
@@ -62,9 +62,10 @@ const WysiwygAutocomplete = forwardRef(
             // TODO handle all of the completion types
             // Using this to pick out the ones we can handle during implementation
             if (completion.type === "command") {
-                // trim the completion text as we add the trailing space in the rust model
-                // nb there are utils like parseCommandString and the CommandMap in SlashCommands.tsx
-                // that might be required here, but for now just use the trimmed completion text
+                // TODO determine if utils in SlashCommands.tsx are required
+
+                // trim the completion as some include trailing spaces, but we always insert a
+                // trailing space in the rust model anyway
                 handleCommand(completion.completion.trim());
             }
             if (client && room && completion.href && (completion.type === "room" || completion.type === "user")) {
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts
index ffb354dda7c..e646046e599 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts
@@ -45,9 +45,9 @@ export function useEditing(
         [initialContent],
     );
 
-    const editMessageMemoized = useCallback(() => {
+    const editMessageMemoized = useCallback(async () => {
         if (mxClient === undefined || content === undefined) {
-            return Promise.resolve(undefined);
+            return;
         }
         return editMessage(content, { roomContext, mxClient, editorStateTransfer });
     }, [content, roomContext, mxClient, editorStateTransfer]);
diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 587ce38f3fa..0d69cb53c42 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -37,16 +37,8 @@ import { CommandCategories, getCommand } from "../../../../../SlashCommands";
 import { runSlashCommand, shouldSendAnyway } from "../../../../../editor/commands";
 import { Action } from "../../../../../dispatcher/actions";
 import { addReplyToMessageContent } from "../../../../../utils/Reply";
+import { attachRelation } from "../../SendMessageComposer";
 
-// Merges favouring the given relation - taken from SendMessageComposer to avoid another import
-function attachRelation(content: IContent, relation?: IEventRelation): void {
-    if (relation) {
-        content["m.relates_to"] = {
-            ...(content["m.relates_to"] || {}),
-            ...relation,
-        };
-    }
-}
 export interface SendMessageParams {
     mxClient: MatrixClient;
     relation?: IEventRelation;
@@ -127,10 +119,8 @@ export async function sendMessage(
         }
     }
 
-    // if content is still null, we haven't done any slash command processing so generate some content
-    if (content === null) {
-        content = await createMessageContent(message, isHTML, params);
-    }
+    // if content is null, we haven't done any slash command processing, so generate some content
+    content ??= await createMessageContent(message, isHTML, params);
 
     // TODO replace emotion end of message ?
 
@@ -149,9 +139,7 @@ export async function sendMessage(
 
     const prom = doMaybeLocalRoomAction(
         roomId,
-        (actualRoomId: string) => {
-            return mxClient.sendMessage(actualRoomId, threadId, content as IContent);
-        },
+        (actualRoomId: string) => mxClient.sendMessage(actualRoomId, threadId, content as IContent),
         mxClient,
     );
 

From 61946a1f4acf37dada07d6a83cec1bbe64fb2a08 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 14:16:33 +0100
Subject: [PATCH 29/31] add TODO

---
 .../rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx   | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
index 20e41fe0b12..709de5fbbc1 100644
--- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
+++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx
@@ -77,6 +77,8 @@ const WysiwygAutocomplete = forwardRef(
             }
         }
 
+        // TODO - determine if we show all of the /command suggestions, there are some options in the
+        // list which don't seem to make sense in this context, specifically /html and /plain
         return room ? (
             <div className="mx_WysiwygComposer_AutoCompleteWrapper" data-testid="autocomplete-wrapper">
                 <Autocomplete

From 276f4f77d75e4e26aae1c78b0663cda311284200 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Thu, 6 Apr 2023 14:37:21 +0100
Subject: [PATCH 30/31] improve comments

---
 .../views/rooms/wysiwyg_composer/utils/message-test.ts      | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
index 8a9dd1617d0..58fc6b7184c 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
@@ -269,8 +269,6 @@ describe("message", () => {
                 expect(getCommandSpy).toHaveBeenCalledTimes(0);
             });
 
-            // TODO amend test when TS fixes are made - this currently can't actually return undefined
-            // according to the TS types
             it("returns undefined when the command is not successful", async () => {
                 // When
                 const validCommand = "/spoiler";
@@ -337,8 +335,6 @@ describe("message", () => {
                 expect(addReplySpy).toHaveBeenCalledTimes(1);
             });
 
-            // TODO - type will change here when I fix the TS errors (maybe)
-
             // these test cases are .action and .admin categories
             const otherCategoryTestCases = ["/nick new_nickname", "/roomname new_room_name"];
             it.each(otherCategoryTestCases)(
@@ -377,7 +373,7 @@ describe("message", () => {
             });
 
             it("if user enters invalid command and then does not send, return undefined", async () => {
-                // mock out returning a true value for `shouldSendAnyway` to avoid rendering the modal
+                // mock out returning a false value for `shouldSendAnyway` to avoid rendering the modal
                 jest.spyOn(Commands, "shouldSendAnyway").mockResolvedValueOnce(false);
                 const invalidCommandInput = "/badCommand";
 

From eb4e1a8b9e1a78579b1190f07bf2a9a1f26252f4 Mon Sep 17 00:00:00 2001
From: Alun Turner <alunt@element.io>
Date: Mon, 10 Apr 2023 08:32:06 +0100
Subject: [PATCH 31/31] update comment

---
 src/components/views/rooms/wysiwyg_composer/utils/message.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 0d69cb53c42..9753ebae494 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -82,7 +82,7 @@ export async function sendMessage(
     if (message.startsWith("/") && !message.startsWith("//")) {
         const { cmd, args } = getCommand(message);
         if (cmd) {
-            // we will need to handle /me separately, see SlashCommands.tsx:1387
+            // TODO handle /me special case separately, see end of SlashCommands.Commands
             const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
             let commandSuccessful: boolean;
             [content, commandSuccessful] = await runSlashCommand(cmd, args, roomId, threadId ?? null);