Add Module Composer API (#33284)

* Spec composer API

* Add composer api implementation

* Tests

* Copyright

* update sigs

* cleanup

* a snap

* cleanup

* linting

* Tidy up

* Adjust
This commit is contained in:
Will Hunt
2026-04-27 10:33:20 +00:00
committed by GitHub
parent cb6c141580
commit 2ea0c4106b
14 changed files with 817 additions and 558 deletions
@@ -1293,23 +1293,30 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
case Action.ComposerInsert: {
if (payload.composerType) break;
const composerInsertPayload = payload as ComposerInsertPayload;
if (composerInsertPayload.composerType) break;
let timelineRenderingType: TimelineRenderingType = payload.timelineRenderingType;
let timelineRenderingType: TimelineRenderingType | undefined;
// ThreadView handles Action.ComposerInsert itself due to it having its own editState
if (timelineRenderingType === TimelineRenderingType.Thread) break;
if (composerInsertPayload.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();
timelineRenderingType = TimelineRenderingType.Room;
}
// If the dispatchee didn't request a timeline rendering type, use the current one.
timelineRenderingType =
timelineRenderingType ??
composerInsertPayload.timelineRenderingType ??
this.state.timelineRenderingType;
// re-dispatch to the correct composer
defaultDispatcher.dispatch<ComposerInsertPayload>({
...(payload as ComposerInsertPayload),
...composerInsertPayload,
timelineRenderingType,
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
});
@@ -167,12 +167,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
switch (payload.action) {
case Action.ComposerInsert: {
if (payload.composerType) break;
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) break;
const insertPayload = payload as ComposerInsertPayload;
if (insertPayload.composerType) break;
if (insertPayload.timelineRenderingType !== TimelineRenderingType.Thread) break;
// re-dispatch to the correct composer
dis.dispatch<ComposerInsertPayload>({
...(payload as ComposerInsertPayload),
...insertPayload,
timelineRenderingType: TimelineRenderingType.Thread,
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
});
break;
@@ -17,7 +17,7 @@ export enum ComposerType {
interface IBaseComposerInsertPayload extends ActionPayload {
action: Action.ComposerInsert;
timelineRenderingType: TimelineRenderingType;
timelineRenderingType?: TimelineRenderingType; // undefined if this should just use the current in-focus type.
composerType?: ComposerType; // falsy if should be re-dispatched to the correct composer
}
+3
View File
@@ -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 = <T extends object>(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);
+23
View File
@@ -0,0 +1,23 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type ComposerApi as ModuleComposerApi } from "@element-hq/element-web-module-api";
import type { MatrixDispatcher } from "../dispatcher/dispatcher";
import { Action } from "../dispatcher/actions";
import type { ComposerInsertPayload } from "../dispatcher/payloads/ComposerInsertPayload";
export class ComposerApi implements ModuleComposerApi {
public constructor(private readonly dispatcher: MatrixDispatcher) {}
public insertPlaintextIntoComposer(plaintext: string): void {
this.dispatcher.dispatch({
action: Action.ComposerInsert,
text: plaintext,
} satisfies ComposerInsertPayload);
}
}