Add support for Module API 1.4 (#30185)

* Add support for Module API 1.3.0

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add missing import

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix import

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Bump module API

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update module API and remove jest stub

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix test mocks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2025-08-11 09:15:45 +01:00
committed by GitHub
parent bbb179b6d3
commit d5a9b3f4c0
18 changed files with 336 additions and 14 deletions

View File

@@ -21,7 +21,11 @@ import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissi
import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts";
import { ConfigApi } from "./ConfigApi.ts";
import { I18nApi } from "./I18nApi.ts";
import { CustomComponentsApi } from "./customComponentApi.ts";
import { CustomComponentsApi } from "./customComponentApi";
import { WatchableProfile } from "./Profile.ts";
import { NavigationApi } from "./Navigation.ts";
import { openDialog } from "./Dialog.tsx";
import { overwriteAccountAuth } from "./Auth.ts";
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
let used = false;
@@ -57,6 +61,11 @@ class ModuleApi implements Api {
legacyCustomisationsFactory(WidgetVariableCustomisations);
/* eslint-enable @typescript-eslint/naming-convention */
public readonly navigation = new NavigationApi();
public readonly openDialog = openDialog;
public readonly overwriteAccountAuth = overwriteAccountAuth;
public readonly profile = new WatchableProfile();
public readonly config = new ConfigApi();
public readonly i18n = new I18nApi();
public readonly customComponents = new CustomComponentsApi();

43
src/modules/Auth.ts Normal file
View File

@@ -0,0 +1,43 @@
/*
Copyright 2025 New Vector 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 AccountAuthInfo } from "@element-hq/element-web-module-api";
import { sleep } from "matrix-js-sdk/src/utils";
import type { OverwriteLoginPayload } from "../dispatcher/payloads/OverwriteLoginPayload.ts";
import { Action } from "../dispatcher/actions.ts";
import defaultDispatcher from "../dispatcher/dispatcher.ts";
import type { ActionPayload } from "../dispatcher/payloads.ts";
export async function overwriteAccountAuth(accountInfo: AccountAuthInfo): Promise<void> {
const { promise, resolve } = Promise.withResolvers<void>();
const onAction = (payload: ActionPayload): void => {
if (payload.action === Action.OnLoggedIn) {
// We want to wait for the new login to complete before returning.
// See `Action.OnLoggedIn` in dispatcher.
resolve();
}
};
const dispatcherRef = defaultDispatcher.register(onAction);
defaultDispatcher.dispatch<OverwriteLoginPayload>(
{
action: Action.OverwriteLogin,
credentials: {
...accountInfo,
guest: false,
},
},
true,
); // require to be sync to match inherited interface behaviour
// wait for login to complete
await promise;
defaultDispatcher.unregister(dispatcherRef);
await sleep(0); // wait for the next tick to ensure the login is fully processed
}

52
src/modules/Dialog.tsx Normal file
View File

@@ -0,0 +1,52 @@
/*
Copyright 2025 New Vector 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 React, { type ComponentType, type JSX, useCallback } from "react";
import { type DialogProps, type DialogOptions, type DialogHandle } from "@element-hq/element-web-module-api";
import Modal from "../Modal";
import BaseDialog from "../components/views/dialogs/BaseDialog.tsx";
const OuterDialog = <M, P extends object>({
title,
Dialog,
props,
onFinished,
}: {
title: string;
Dialog: ComponentType<DialogProps<M> & P>;
props: P;
onFinished(ok: boolean, model: M | null): void;
}): JSX.Element => {
const close = useCallback(() => onFinished(false, null), [onFinished]);
const submit = useCallback((model: M) => onFinished(true, model), [onFinished]);
return (
<BaseDialog onFinished={close} title={title}>
<Dialog {...props} onSubmit={submit} onCancel={close} />
</BaseDialog>
);
};
export function openDialog<M, P extends object>(
initialOptions: DialogOptions,
Dialog: ComponentType<P & DialogProps<M>>,
props: P,
): DialogHandle<M> {
const { close, finished } = Modal.createDialog(OuterDialog<M, P>, {
title: initialOptions.title,
Dialog,
props,
});
return {
finished: finished.then(([ok, model]) => ({
ok: ok ?? false,
model: model ?? null,
})),
close: () => close(false, null),
};
}

43
src/modules/Navigation.ts Normal file
View File

@@ -0,0 +1,43 @@
/*
Copyright 2025 New Vector 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 NavigationApi as INavigationApi } from "@element-hq/element-web-module-api";
import { navigateToPermalink } from "../utils/permalinks/navigator.ts";
import { parsePermalink } from "../utils/permalinks/Permalinks.ts";
import { getCachedRoomIDForAlias } from "../RoomAliasCache.ts";
import { MatrixClientPeg } from "../MatrixClientPeg.ts";
import dispatcher from "../dispatcher/dispatcher.ts";
import { Action } from "../dispatcher/actions.ts";
import SettingsStore from "../settings/SettingsStore.ts";
export class NavigationApi implements INavigationApi {
public async toMatrixToLink(link: string, join = false): Promise<void> {
navigateToPermalink(link);
const parts = parsePermalink(link);
if (parts?.roomIdOrAlias && join) {
let roomId: string | undefined = parts.roomIdOrAlias;
if (roomId.startsWith("#")) {
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
if (!roomId) {
// alias resolution failed
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
roomId = result.room_id;
}
}
if (roomId) {
dispatcher.dispatch({
action: Action.JoinRoom,
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
roomId,
});
}
}
}
}

32
src/modules/Profile.ts Normal file
View File

@@ -0,0 +1,32 @@
/*
Copyright 2025 New Vector 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 Profile, Watchable } from "@element-hq/element-web-module-api";
import { OwnProfileStore } from "../stores/OwnProfileStore.ts";
import { UPDATE_EVENT } from "../stores/AsyncStore.ts";
export class WatchableProfile extends Watchable<Profile> {
public constructor() {
super({});
this.value = this.profile;
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileChange);
}
private get profile(): Profile {
return {
isGuest: OwnProfileStore.instance.matrixClient?.isGuest() ?? false,
userId: OwnProfileStore.instance.matrixClient?.getUserId() ?? undefined,
displayName: OwnProfileStore.instance.displayName ?? undefined,
};
}
private readonly onProfileChange = (): void => {
this.value = this.profile;
};
}

View File

@@ -12,9 +12,10 @@ import type {
CustomComponentsApi as ICustomComponentsApi,
CustomMessageRenderFunction,
CustomMessageComponentProps as ModuleCustomMessageComponentProps,
OriginalComponentProps,
OriginalMessageComponentProps,
CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints,
MatrixEvent as ModuleMatrixEvent,
CustomRoomPreviewBarRenderFunction,
} from "@element-hq/element-web-module-api";
import type React from "react";
@@ -72,6 +73,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
): void {
this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints });
}
/**
* Select the correct renderer based on the event information.
* @param mxEvent The message event being rendered.
@@ -100,7 +102,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
*/
public renderMessage(
props: CustomMessageComponentProps,
originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element,
originalComponent?: (props?: OriginalMessageComponentProps) => React.JSX.Element,
): React.JSX.Element | null {
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(props.mxEvent);
const renderer = moduleEv && this.selectRenderer(moduleEv);
@@ -134,4 +136,21 @@ export class CustomComponentsApi implements ICustomComponentsApi {
}
return null;
}
private _roomPreviewBarRenderer?: CustomRoomPreviewBarRenderFunction;
/**
* Get the custom room preview bar renderer, if any has been registered.
*/
public get roomPreviewBarRenderer(): CustomRoomPreviewBarRenderFunction | undefined {
return this._roomPreviewBarRenderer;
}
/**
* Register a custom room preview bar renderer.
* @param renderer - the function that will render the custom room preview bar.
*/
public registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void {
this._roomPreviewBarRenderer = renderer;
}
}