Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "1.7.0",
"@element-hq/element-web-module-api": "1.8.0",
"@element-hq/web-shared-components": "link:packages/shared-components",
"@fontsource/fira-code": "^5",
"@fontsource/inter": "^5",
Expand Down
23 changes: 21 additions & 2 deletions src/modules/BuiltinsApi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { type RoomViewProps, type BuiltinsApi } from "@element-hq/element-web-mo

import { MatrixClientPeg } from "../MatrixClientPeg";
import type { Room } from "matrix-js-sdk/src/matrix";
import type { ModuleNotificationDecorationProps } from "./components/ModuleNotificationDecoration";

interface RoomViewPropsWithRoomId extends RoomViewProps {
/**
Expand All @@ -26,11 +27,14 @@ interface RoomAvatarProps {
interface Components {
roomView: React.ComponentType<RoomViewPropsWithRoomId>;
roomAvatar: React.ComponentType<RoomAvatarProps>;
notificationDecoration: React.ComponentType<ModuleNotificationDecorationProps>;
}

export class ElementWebBuiltinsApi implements BuiltinsApi {
private _roomView?: Components["roomView"];
private _roomAvatar?: Components["roomAvatar"];
private _notificationDecoration?: Components["notificationDecoration"];

/**
* Sets the components used by the API.
*
Expand All @@ -43,24 +47,30 @@ export class ElementWebBuiltinsApi implements BuiltinsApi {
public setComponents(components: Components): void {
this._roomView = components.roomView;
this._roomAvatar = components.roomAvatar;
this._notificationDecoration = components.notificationDecoration;
}

public getRoomViewComponent(): React.ComponentType<RoomViewPropsWithRoomId> {
if (!this._roomView) {
throw new Error("No RoomView component has been set");
}

return this._roomView;
}

public getRoomAvatarComponent(): React.ComponentType<RoomAvatarProps> {
if (!this._roomAvatar) {
throw new Error("No RoomAvatar component has been set");
}

return this._roomAvatar;
}

public getNotificationDecorationComponent(): React.ComponentType<ModuleNotificationDecorationProps> {
if (!this._notificationDecoration) {
throw new Error("No NotificationDecoration component has been set");
}
return this._notificationDecoration;
}

public renderRoomView(roomId: string, props?: RoomViewProps): React.ReactNode {
const Component = this.getRoomViewComponent();
return <Component roomId={roomId} {...props} />;
Expand All @@ -74,4 +84,13 @@ export class ElementWebBuiltinsApi implements BuiltinsApi {
const Component = this.getRoomAvatarComponent();
return <Component room={room} size={size} />;
}

public renderNotificationDecoration(roomId: string): React.ReactNode {
const room = MatrixClientPeg.safeGet().getRoom(roomId);
if (!room) {
throw new Error(`No room such room: ${roomId}`);
}
const Component = this.getNotificationDecorationComponent();
return <Component room={room} />;
}
}
29 changes: 29 additions & 0 deletions src/modules/components/ModuleNotificationDecoration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2025 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 React, { useMemo } from "react";

import type { Room } from "matrix-js-sdk/src/matrix";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { useCall } from "../../hooks/useCall";
import { NotificationDecoration } from "../../components/views/rooms/NotificationDecoration";

export interface ModuleNotificationDecorationProps {
/**
* The room for which the decoration is rendered.
*/
room: Room;
}

/**
* React component that takes a room as prop and renders {@link NotificationDecoration} with it.
* Used by the module API to render notification decoration without having to expose a bunch of stores.
*/
export const ModuleNotificationDecoration: React.FC<ModuleNotificationDecorationProps> = ({ room }) => {
const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]);
const call = useCall(room.roomId);
return <NotificationDecoration notificationState={notificationState} callType={call?.callType} />;
};
7 changes: 6 additions & 1 deletion src/vector/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { UserFriendlyError } from "../languageHandler";
import { ModuleApi } from "../modules/Api";
import { RoomView } from "../components/structures/RoomView";
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import { ModuleNotificationDecoration } from "../modules/components/ModuleNotificationDecoration";

logger.log(`Application is running in ${process.env.NODE_ENV} mode`);

Expand All @@ -58,7 +59,11 @@ function onTokenLoginCompleted(): void {
export async function loadApp(fragParams: QueryDict, matrixChatRef: React.Ref<MatrixChat>): Promise<ReactElement> {
// XXX: This lives here because certain components import so many things that importing it in a sensible place (eg.
// the builtins module or init.tsx) causes a circular dependency.
ModuleApi.instance.builtins.setComponents({ roomView: RoomView, roomAvatar: RoomAvatar });
ModuleApi.instance.builtins.setComponents({
roomView: RoomView,
roomAvatar: RoomAvatar,
notificationDecoration: ModuleNotificationDecoration,
});

initRouting();
const platform = PlatformPeg.get();
Expand Down
16 changes: 16 additions & 0 deletions test/unit-tests/modules/BuiltinsApi-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,26 @@ describe("ElementWebBuiltinsApi", () => {
expect(container).toHaveTextContent("50");
});

it("returns rendered NotificationDecoration component", () => {
stubClient();
const builtinsApi = new ElementWebBuiltinsApi();
const NotificationDecoration = () => <div>notification decoration</div>;
builtinsApi.setComponents({
roomView: {},
roomAvatar: Avatar,
notificationDecoration: NotificationDecoration,
} as any);
const { container } = render(<> {builtinsApi.renderNotificationDecoration("!foo:m.org")}</>);
expect(container).toHaveTextContent("notification decoration");
});

it("should throw error if called before components are set", () => {
stubClient();
const builtinsApi = new ElementWebBuiltinsApi();
expect(() => builtinsApi.renderRoomAvatar("!foo:m.org")).toThrow("No RoomAvatar component has been set");
expect(() => builtinsApi.renderRoomView("!foo:m.org")).toThrow("No RoomView component has been set");
expect(() => builtinsApi.renderNotificationDecoration("!foo:m.org")).toThrow(
"No NotificationDecoration component has been set",
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
Copyright 2025 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 React from "react";
import { render, screen } from "jest-matrix-react";

import type { Room } from "matrix-js-sdk/src/matrix";
import { ModuleNotificationDecoration } from "../../../../src/modules/components/ModuleNotificationDecoration";
import { mkStubRoom, stubClient } from "../../../test-utils";
import { NotificationLevel } from "../../../../src/stores/notifications/NotificationLevel";
import { RoomNotificationStateStore } from "../../../../src/stores/notifications/RoomNotificationStateStore";
import { RoomNotificationState } from "../../../../src/stores/notifications/RoomNotificationState";

class MockedNotificationState extends RoomNotificationState {
public constructor(room: Room, level: NotificationLevel, count: number) {
super(room, false);
this._level = level;
this._count = count;
}
}

it("Should be able to render component just with room as prop", () => {
const cli = stubClient();
const room = mkStubRoom("!foo:matrix.org", "Foo Room", cli);
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(
new MockedNotificationState(room, NotificationLevel.Notification, 5),
);
render(<ModuleNotificationDecoration room={room} />);
expect(screen.getByTestId("notification-decoration")).toBeInTheDocument();
});
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1559,10 +1559,10 @@
resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.16.1.tgz#28bdbde426051cc2a3228a36e7196e0a254569d3"
integrity sha512-g3v/QFuNy8YVRGrKC5SxjIYvgBh6biOHgejhJT2Jk/yjOOUEuP0y2PBaADm+suPD9BB/Vk1jPxFk2uEIpEzhpA==

"@element-hq/element-web-module-api@1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.7.0.tgz#7657df25cc1e7075718af2c6ea8a4ebfaa9cfb2c"
integrity sha512-WhiJTmdETK8vvaYExqyhQ9rtLjxBv9PprWr6dCa1/1VRFSkfFZRlzy2P08nHX2YXpRMTpXb39SLeleR1dgLzow==
"@element-hq/element-web-module-api@1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.8.0.tgz#95aa4ec22609cf0f4a7f24274473af0645a16f2a"
integrity sha512-lMiDA9ubP3mZZupIMT8T3wS0riX30rYZj3pFpdP4cfZhkWZa3FJFStokAy5OnaHyENC7Px1cqkBGqilOWewY/A==

"@element-hq/element-web-playwright-common@^2.0.0":
version "2.0.0"
Expand Down Expand Up @@ -4151,7 +4151,7 @@
classnames "^2.5.1"
vaul "^1.0.0"

"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm":
"@vector-im/matrix-wysiwyg-wasm@link:../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm":
version "0.0.0"
uid ""

Expand Down
Loading