diff --git a/package.json b/package.json index c3bd5f800a..e87aabbb94 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/modules/BuiltinsApi.tsx b/src/modules/BuiltinsApi.tsx index 9ede5ef99a..83d4c2acc4 100644 --- a/src/modules/BuiltinsApi.tsx +++ b/src/modules/BuiltinsApi.tsx @@ -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 { /** @@ -26,11 +27,14 @@ interface RoomAvatarProps { interface Components { roomView: React.ComponentType; roomAvatar: React.ComponentType; + notificationDecoration: React.ComponentType; } export class ElementWebBuiltinsApi implements BuiltinsApi { private _roomView?: Components["roomView"]; private _roomAvatar?: Components["roomAvatar"]; + private _notificationDecoration?: Components["notificationDecoration"]; + /** * Sets the components used by the API. * @@ -43,13 +47,13 @@ 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 { if (!this._roomView) { throw new Error("No RoomView component has been set"); } - return this._roomView; } @@ -57,10 +61,16 @@ export class ElementWebBuiltinsApi implements BuiltinsApi { if (!this._roomAvatar) { throw new Error("No RoomAvatar component has been set"); } - return this._roomAvatar; } + public getNotificationDecorationComponent(): React.ComponentType { + 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 ; @@ -74,4 +84,13 @@ export class ElementWebBuiltinsApi implements BuiltinsApi { const Component = this.getRoomAvatarComponent(); return ; } + + 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 ; + } } diff --git a/src/modules/components/ModuleNotificationDecoration.tsx b/src/modules/components/ModuleNotificationDecoration.tsx new file mode 100644 index 0000000000..12852d0910 --- /dev/null +++ b/src/modules/components/ModuleNotificationDecoration.tsx @@ -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 = ({ room }) => { + const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); + const call = useCall(room.roomId); + return ; +}; diff --git a/src/vector/app.tsx b/src/vector/app.tsx index 870b51aff8..84f9b18ec1 100644 --- a/src/vector/app.tsx +++ b/src/vector/app.tsx @@ -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`); @@ -58,7 +59,11 @@ function onTokenLoginCompleted(): void { export async function loadApp(fragParams: QueryDict, matrixChatRef: React.Ref): Promise { // 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(); diff --git a/test/unit-tests/modules/BuiltinsApi-test.tsx b/test/unit-tests/modules/BuiltinsApi-test.tsx index 2b3b1139a5..e84263aa4d 100644 --- a/test/unit-tests/modules/BuiltinsApi-test.tsx +++ b/test/unit-tests/modules/BuiltinsApi-test.tsx @@ -44,10 +44,26 @@ describe("ElementWebBuiltinsApi", () => { expect(container).toHaveTextContent("50"); }); + it("returns rendered NotificationDecoration component", () => { + stubClient(); + const builtinsApi = new ElementWebBuiltinsApi(); + const NotificationDecoration = () =>
notification decoration
; + 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", + ); }); }); diff --git a/test/unit-tests/modules/components/ModuleNotificationDecoration-test.tsx b/test/unit-tests/modules/components/ModuleNotificationDecoration-test.tsx new file mode 100644 index 0000000000..65df1af1f7 --- /dev/null +++ b/test/unit-tests/modules/components/ModuleNotificationDecoration-test.tsx @@ -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(); + expect(screen.getByTestId("notification-decoration")).toBeInTheDocument(); +}); diff --git a/yarn.lock b/yarn.lock index 74333404e8..9eaf83fc32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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 ""