diff --git a/apps/web/src/components/structures/RoomView.tsx b/apps/web/src/components/structures/RoomView.tsx index 3704cf5885..fc30c1ecfb 100644 --- a/apps/web/src/components/structures/RoomView.tsx +++ b/apps/web/src/components/structures/RoomView.tsx @@ -1293,14 +1293,17 @@ export class RoomView extends React.Component { } case Action.ComposerInsert: { - if (payload.composerType) break; + const composerInsertPayload = payload as ComposerInsertPayload; + if (composerInsertPayload.composerType) break; - let timelineRenderingType: TimelineRenderingType = payload.timelineRenderingType; + // If the dispatchee didn't request a timeline rendering type, use the current one. + let timelineRenderingType: TimelineRenderingType = + composerInsertPayload.timelineRenderingType ?? this.state.timelineRenderingType; // ThreadView handles Action.ComposerInsert itself due to it having its own editState if (timelineRenderingType === TimelineRenderingType.Thread) break; if ( this.state.timelineRenderingType === TimelineRenderingType.Search && - payload.timelineRenderingType === TimelineRenderingType.Search + composerInsertPayload.timelineRenderingType === TimelineRenderingType.Search ) { // we don't have the composer rendered in this state, so bring it back first await this.onCancelSearchClick(); @@ -1309,7 +1312,7 @@ export class RoomView extends React.Component { // re-dispatch to the correct composer defaultDispatcher.dispatch({ - ...(payload as ComposerInsertPayload), + ...composerInsertPayload, timelineRenderingType, composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send, }); diff --git a/apps/web/src/dispatcher/payloads/ComposerInsertPayload.ts b/apps/web/src/dispatcher/payloads/ComposerInsertPayload.ts index 9712a8303a..a825c13c3a 100644 --- a/apps/web/src/dispatcher/payloads/ComposerInsertPayload.ts +++ b/apps/web/src/dispatcher/payloads/ComposerInsertPayload.ts @@ -15,18 +15,32 @@ export enum ComposerType { Edit = "edit", } -interface IBaseComposerInsertPayload extends ActionPayload { +/** + * Explicit composer insert to target + */ +interface IBaseComposerInsertPayloadExplicit extends ActionPayload { action: Action.ComposerInsert; timelineRenderingType: TimelineRenderingType; - composerType?: ComposerType; // falsy if should be re-dispatched to the correct composer + composerType: ComposerType; } -interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload { +/** + * Explicit composer insert to current target. + */ +interface IBaseComposerInsertPayloadImplicit extends ActionPayload { + action: Action.ComposerInsert; + composerType?: undefined; // undefined if this should be re-dispatched to the correct composer + timelineRenderingType?: TimelineRenderingType; // undefined if this should just use the current in-focus type. +} + +type IBaseComposerInsertPayload = IBaseComposerInsertPayloadExplicit | IBaseComposerInsertPayloadImplicit; + +type IComposerInsertMentionPayload = IBaseComposerInsertPayload & { userId: string; -} +}; -interface IComposerInsertPlaintextPayload extends IBaseComposerInsertPayload { +type IComposerInsertPlaintextPayload = IBaseComposerInsertPayload & { text: string; -} +}; export type ComposerInsertPayload = IComposerInsertMentionPayload | IComposerInsertPlaintextPayload; diff --git a/apps/web/src/modules/Api.ts b/apps/web/src/modules/Api.ts index bb3c7497d5..5d9eddfab2 100644 --- a/apps/web/src/modules/Api.ts +++ b/apps/web/src/modules/Api.ts @@ -33,6 +33,8 @@ import { StoresApi } from "./StoresApi.ts"; import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts"; import { WidgetApi } from "./WidgetApi.ts"; import { CustomisationsApi } from "./customisationsApi.ts"; +import { ComposerApi } from "./ComposerApi.ts"; +import defaultDispatcher from "../dispatcher/dispatcher.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false; @@ -94,6 +96,7 @@ export class ModuleApi implements Api { public readonly rootNode = document.getElementById("matrixchat")!; public readonly client = new ClientApi(); public readonly stores = new StoresApi(); + public readonly composer = new ComposerApi(defaultDispatcher); public createRoot(element: Element): Root { return createRoot(element); diff --git a/apps/web/src/modules/ComposerApi.ts b/apps/web/src/modules/ComposerApi.ts index 5442d69d57..5c83de4eb0 100644 --- a/apps/web/src/modules/ComposerApi.ts +++ b/apps/web/src/modules/ComposerApi.ts @@ -7,17 +7,17 @@ Please see LICENSE files in the repository root for full details. import { type ComposerApi as ModuleComposerApi } from "@element-hq/element-web-module-api"; -import defaultDispatcher from "../dispatcher/dispatcher"; +import type { MatrixDispatcher } from "../dispatcher/dispatcher"; import { Action } from "../dispatcher/actions"; import type { ComposerInsertPayload } from "../dispatcher/payloads/ComposerInsertPayload"; -import { TimelineRenderingType } from "../contexts/RoomContext"; export class ComposerApi implements ModuleComposerApi { + public constructor(private readonly dispatcher: MatrixDispatcher) {} + public insertTextIntoComposer(text: string): void { - defaultDispatcher.dispatch({ + this.dispatcher.dispatch({ action: Action.ComposerInsert, text, - timelineRenderingType: TimelineRenderingType.Room, } satisfies ComposerInsertPayload); } } diff --git a/apps/web/test/unit-tests/components/structures/RoomView-test.tsx b/apps/web/test/unit-tests/components/structures/RoomView-test.tsx index 77bbdd8d47..50d229a26e 100644 --- a/apps/web/test/unit-tests/components/structures/RoomView-test.tsx +++ b/apps/web/test/unit-tests/components/structures/RoomView-test.tsx @@ -71,6 +71,7 @@ import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog.ts import * as pinnedEventHooks from "../../../../src/hooks/usePinnedEvents"; import { TimelineRenderingType } from "../../../../src/contexts/RoomContext"; import { ModuleApi } from "../../../../src/modules/Api"; +import { ComposerInsertPayload, ComposerType } from "../../../../src/dispatcher/payloads/ComposerInsertPayload.ts"; // Used by group calls jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ @@ -1075,6 +1076,54 @@ describe("RoomView", () => { expect(onRoomViewUpdateMock).toHaveBeenCalledWith(true); }); + describe("handles Action.ComposerInsert", () => { + it("redispatches an empty composerType, timelineRenderingType with the current state", async () => { + jest.spyOn(defaultDispatcher, "dispatch"); + await mountRoomView(); + const promise = untilDispatch((payload) => { + try { + expect(payload).toEqual({ + action: Action.ComposerInsert, + text: "Hello world", + timelineRenderingType: TimelineRenderingType.Room, + composerType: ComposerType.Send, + }); + } catch { + return false; + } + return true; + }, defaultDispatcher); + defaultDispatcher.dispatch({ + action: Action.ComposerInsert, + text: "Hello world", + } satisfies ComposerInsertPayload); + await promise; + }); + it("redispatches an empty composerType with the current state", async () => { + jest.spyOn(defaultDispatcher, "dispatch"); + await mountRoomView(); + const promise = untilDispatch((payload) => { + try { + expect(payload).toEqual({ + action: Action.ComposerInsert, + text: "Hello world", + timelineRenderingType: TimelineRenderingType.Room, + composerType: ComposerType.Send, + }); + } catch { + return false; + } + return true; + }, defaultDispatcher); + defaultDispatcher.dispatch({ + action: Action.ComposerInsert, + text: "Hello world", + timelineRenderingType: TimelineRenderingType.Room, + } satisfies ComposerInsertPayload); + await promise; + }); + }); + describe("when there is a RoomView", () => { const widget1Id = "widget1"; const widget2Id = "widget2"; diff --git a/apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index 40df5cb41e..583012faa4 100644 --- a/apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -247,9 +247,9 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] class="_actions_n7ud0_61" >