Skip to content

Commit d5a9b3f

Browse files
authored
Add support for Module API 1.4 (#30185)
* Add support for Module API 1.3.0 Signed-off-by: Michael Telatynski <[email protected]> * Add missing import Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Fix import Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Bump module API Signed-off-by: Michael Telatynski <[email protected]> * Update module API and remove jest stub Signed-off-by: Michael Telatynski <[email protected]> * Fix test mocks Signed-off-by: Michael Telatynski <[email protected]> * Improve coverage Signed-off-by: Michael Telatynski <[email protected]> * types Signed-off-by: Michael Telatynski <[email protected]> * Coverage Signed-off-by: Michael Telatynski <[email protected]> * Coverage Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent bbb179b commit d5a9b3f

File tree

18 files changed

+336
-14
lines changed

18 files changed

+336
-14
lines changed

jest.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ const config: Config = {
4040
"^!!raw-loader!.*": "jest-raw-loader",
4141
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
4242
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
43-
// Requires ESM which is incompatible with our current Jest setup
44-
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
4543
},
4644
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
4745
collectCoverageFrom: [

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
},
8787
"dependencies": {
8888
"@babel/runtime": "^7.12.5",
89-
"@element-hq/element-web-module-api": "1.3.0",
89+
"@element-hq/element-web-module-api": "1.4.1",
9090
"@fontsource/inconsolata": "^5",
9191
"@fontsource/inter": "^5",
9292
"@formatjs/intl-segmenter": "^11.5.7",

src/components/views/rooms/RoomPreviewBar.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { UIFeature } from "../../../settings/UIFeature";
3131
import { ModuleRunner } from "../../../modules/ModuleRunner";
3232
import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg";
3333
import Field from "../elements/Field";
34+
import ModuleApi from "../../../modules/Api.ts";
3435

3536
const MemberEventHtmlReasonField = "io.element.html_reason";
3637

@@ -116,7 +117,7 @@ interface IState {
116117
reason?: string;
117118
}
118119

119-
export default class RoomPreviewBar extends React.Component<IProps, IState> {
120+
class RoomPreviewBar extends React.Component<IProps, IState> {
120121
public static defaultProps = {
121122
onJoinClick() {},
122123
};
@@ -747,3 +748,21 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
747748
);
748749
}
749750
}
751+
752+
const WrappedRoomPreviewBar = (props: IProps): JSX.Element => {
753+
const moduleRenderer = ModuleApi.customComponents.roomPreviewBarRenderer;
754+
if (moduleRenderer) {
755+
return moduleRenderer(
756+
{
757+
...props,
758+
roomId: props.room?.roomId ?? props.roomId,
759+
roomAlias: props.room?.getCanonicalAlias() ?? props.roomAlias,
760+
},
761+
(props) => <RoomPreviewBar {...props} />,
762+
);
763+
}
764+
765+
return <RoomPreviewBar {...props} />;
766+
};
767+
768+
export default WrappedRoomPreviewBar;

src/modules/Api.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissi
2121
import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts";
2222
import { ConfigApi } from "./ConfigApi.ts";
2323
import { I18nApi } from "./I18nApi.ts";
24-
import { CustomComponentsApi } from "./customComponentApi.ts";
24+
import { CustomComponentsApi } from "./customComponentApi";
25+
import { WatchableProfile } from "./Profile.ts";
26+
import { NavigationApi } from "./Navigation.ts";
27+
import { openDialog } from "./Dialog.tsx";
28+
import { overwriteAccountAuth } from "./Auth.ts";
2529

2630
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
2731
let used = false;
@@ -57,6 +61,11 @@ class ModuleApi implements Api {
5761
legacyCustomisationsFactory(WidgetVariableCustomisations);
5862
/* eslint-enable @typescript-eslint/naming-convention */
5963

64+
public readonly navigation = new NavigationApi();
65+
public readonly openDialog = openDialog;
66+
public readonly overwriteAccountAuth = overwriteAccountAuth;
67+
public readonly profile = new WatchableProfile();
68+
6069
public readonly config = new ConfigApi();
6170
public readonly i18n = new I18nApi();
6271
public readonly customComponents = new CustomComponentsApi();

src/modules/Auth.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { type AccountAuthInfo } from "@element-hq/element-web-module-api";
9+
import { sleep } from "matrix-js-sdk/src/utils";
10+
11+
import type { OverwriteLoginPayload } from "../dispatcher/payloads/OverwriteLoginPayload.ts";
12+
import { Action } from "../dispatcher/actions.ts";
13+
import defaultDispatcher from "../dispatcher/dispatcher.ts";
14+
import type { ActionPayload } from "../dispatcher/payloads.ts";
15+
16+
export async function overwriteAccountAuth(accountInfo: AccountAuthInfo): Promise<void> {
17+
const { promise, resolve } = Promise.withResolvers<void>();
18+
19+
const onAction = (payload: ActionPayload): void => {
20+
if (payload.action === Action.OnLoggedIn) {
21+
// We want to wait for the new login to complete before returning.
22+
// See `Action.OnLoggedIn` in dispatcher.
23+
resolve();
24+
}
25+
};
26+
const dispatcherRef = defaultDispatcher.register(onAction);
27+
28+
defaultDispatcher.dispatch<OverwriteLoginPayload>(
29+
{
30+
action: Action.OverwriteLogin,
31+
credentials: {
32+
...accountInfo,
33+
guest: false,
34+
},
35+
},
36+
true,
37+
); // require to be sync to match inherited interface behaviour
38+
39+
// wait for login to complete
40+
await promise;
41+
defaultDispatcher.unregister(dispatcherRef);
42+
await sleep(0); // wait for the next tick to ensure the login is fully processed
43+
}

src/modules/Dialog.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React, { type ComponentType, type JSX, useCallback } from "react";
9+
import { type DialogProps, type DialogOptions, type DialogHandle } from "@element-hq/element-web-module-api";
10+
11+
import Modal from "../Modal";
12+
import BaseDialog from "../components/views/dialogs/BaseDialog.tsx";
13+
14+
const OuterDialog = <M, P extends object>({
15+
title,
16+
Dialog,
17+
props,
18+
onFinished,
19+
}: {
20+
title: string;
21+
Dialog: ComponentType<DialogProps<M> & P>;
22+
props: P;
23+
onFinished(ok: boolean, model: M | null): void;
24+
}): JSX.Element => {
25+
const close = useCallback(() => onFinished(false, null), [onFinished]);
26+
const submit = useCallback((model: M) => onFinished(true, model), [onFinished]);
27+
return (
28+
<BaseDialog onFinished={close} title={title}>
29+
<Dialog {...props} onSubmit={submit} onCancel={close} />
30+
</BaseDialog>
31+
);
32+
};
33+
34+
export function openDialog<M, P extends object>(
35+
initialOptions: DialogOptions,
36+
Dialog: ComponentType<P & DialogProps<M>>,
37+
props: P,
38+
): DialogHandle<M> {
39+
const { close, finished } = Modal.createDialog(OuterDialog<M, P>, {
40+
title: initialOptions.title,
41+
Dialog,
42+
props,
43+
});
44+
45+
return {
46+
finished: finished.then(([ok, model]) => ({
47+
ok: ok ?? false,
48+
model: model ?? null,
49+
})),
50+
close: () => close(false, null),
51+
};
52+
}

src/modules/Navigation.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { type NavigationApi as INavigationApi } from "@element-hq/element-web-module-api";
9+
10+
import { navigateToPermalink } from "../utils/permalinks/navigator.ts";
11+
import { parsePermalink } from "../utils/permalinks/Permalinks.ts";
12+
import { getCachedRoomIDForAlias } from "../RoomAliasCache.ts";
13+
import { MatrixClientPeg } from "../MatrixClientPeg.ts";
14+
import dispatcher from "../dispatcher/dispatcher.ts";
15+
import { Action } from "../dispatcher/actions.ts";
16+
import SettingsStore from "../settings/SettingsStore.ts";
17+
18+
export class NavigationApi implements INavigationApi {
19+
public async toMatrixToLink(link: string, join = false): Promise<void> {
20+
navigateToPermalink(link);
21+
22+
const parts = parsePermalink(link);
23+
if (parts?.roomIdOrAlias && join) {
24+
let roomId: string | undefined = parts.roomIdOrAlias;
25+
if (roomId.startsWith("#")) {
26+
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
27+
if (!roomId) {
28+
// alias resolution failed
29+
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
30+
roomId = result.room_id;
31+
}
32+
}
33+
34+
if (roomId) {
35+
dispatcher.dispatch({
36+
action: Action.JoinRoom,
37+
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
38+
roomId,
39+
});
40+
}
41+
}
42+
}
43+
}

src/modules/Profile.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { type Profile, Watchable } from "@element-hq/element-web-module-api";
9+
10+
import { OwnProfileStore } from "../stores/OwnProfileStore.ts";
11+
import { UPDATE_EVENT } from "../stores/AsyncStore.ts";
12+
13+
export class WatchableProfile extends Watchable<Profile> {
14+
public constructor() {
15+
super({});
16+
this.value = this.profile;
17+
18+
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileChange);
19+
}
20+
21+
private get profile(): Profile {
22+
return {
23+
isGuest: OwnProfileStore.instance.matrixClient?.isGuest() ?? false,
24+
userId: OwnProfileStore.instance.matrixClient?.getUserId() ?? undefined,
25+
displayName: OwnProfileStore.instance.displayName ?? undefined,
26+
};
27+
}
28+
29+
private readonly onProfileChange = (): void => {
30+
this.value = this.profile;
31+
};
32+
}

src/modules/customComponentApi.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import type {
1212
CustomComponentsApi as ICustomComponentsApi,
1313
CustomMessageRenderFunction,
1414
CustomMessageComponentProps as ModuleCustomMessageComponentProps,
15-
OriginalComponentProps,
15+
OriginalMessageComponentProps,
1616
CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints,
1717
MatrixEvent as ModuleMatrixEvent,
18+
CustomRoomPreviewBarRenderFunction,
1819
} from "@element-hq/element-web-module-api";
1920
import type React from "react";
2021

@@ -72,6 +73,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
7273
): void {
7374
this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints });
7475
}
76+
7577
/**
7678
* Select the correct renderer based on the event information.
7779
* @param mxEvent The message event being rendered.
@@ -100,7 +102,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
100102
*/
101103
public renderMessage(
102104
props: CustomMessageComponentProps,
103-
originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element,
105+
originalComponent?: (props?: OriginalMessageComponentProps) => React.JSX.Element,
104106
): React.JSX.Element | null {
105107
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(props.mxEvent);
106108
const renderer = moduleEv && this.selectRenderer(moduleEv);
@@ -134,4 +136,21 @@ export class CustomComponentsApi implements ICustomComponentsApi {
134136
}
135137
return null;
136138
}
139+
140+
private _roomPreviewBarRenderer?: CustomRoomPreviewBarRenderFunction;
141+
142+
/**
143+
* Get the custom room preview bar renderer, if any has been registered.
144+
*/
145+
public get roomPreviewBarRenderer(): CustomRoomPreviewBarRenderFunction | undefined {
146+
return this._roomPreviewBarRenderer;
147+
}
148+
149+
/**
150+
* Register a custom room preview bar renderer.
151+
* @param renderer - the function that will render the custom room preview bar.
152+
*/
153+
public registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void {
154+
this._roomPreviewBarRenderer = renderer;
155+
}
137156
}

src/stores/RoomViewStore.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,8 @@ export class RoomViewStore extends EventEmitter {
510510
});
511511

512512
// take a copy of roomAlias & roomId as they may change by the time the join is complete
513-
const { roomAlias, roomId = payload.roomId } = this.state;
514-
const address = roomAlias || roomId!;
513+
const { roomAlias, roomId } = this.state;
514+
const address = payload.roomId || roomAlias || roomId!;
515515

516516
const joinOpts: IJoinRoomOpts = {
517517
viaServers: this.state.viaServers || [],
@@ -520,7 +520,6 @@ export class RoomViewStore extends EventEmitter {
520520
if (SettingsStore.getValue("feature_share_history_on_invite")) {
521521
joinOpts.acceptSharedHistory = true;
522522
}
523-
524523
try {
525524
const cli = MatrixClientPeg.safeGet();
526525
await retry<Room, MatrixError>(

0 commit comments

Comments
 (0)