Implement new renderNotificationDecoration from module API (#31389)
* Upgrade module api package * Add a wrapper component So that we can render the decoration component with just the room. * Implement module API method * Add more tests
This commit is contained in:
+1
-1
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@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",
|
"@element-hq/web-shared-components": "link:packages/shared-components",
|
||||||
"@fontsource/fira-code": "^5",
|
"@fontsource/fira-code": "^5",
|
||||||
"@fontsource/inter": "^5",
|
"@fontsource/inter": "^5",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { type RoomViewProps, type BuiltinsApi } from "@element-hq/element-web-mo
|
|||||||
|
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import type { ModuleNotificationDecorationProps } from "./components/ModuleNotificationDecoration";
|
||||||
|
|
||||||
interface RoomViewPropsWithRoomId extends RoomViewProps {
|
interface RoomViewPropsWithRoomId extends RoomViewProps {
|
||||||
/**
|
/**
|
||||||
@@ -26,11 +27,14 @@ interface RoomAvatarProps {
|
|||||||
interface Components {
|
interface Components {
|
||||||
roomView: React.ComponentType<RoomViewPropsWithRoomId>;
|
roomView: React.ComponentType<RoomViewPropsWithRoomId>;
|
||||||
roomAvatar: React.ComponentType<RoomAvatarProps>;
|
roomAvatar: React.ComponentType<RoomAvatarProps>;
|
||||||
|
notificationDecoration: React.ComponentType<ModuleNotificationDecorationProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ElementWebBuiltinsApi implements BuiltinsApi {
|
export class ElementWebBuiltinsApi implements BuiltinsApi {
|
||||||
private _roomView?: Components["roomView"];
|
private _roomView?: Components["roomView"];
|
||||||
private _roomAvatar?: Components["roomAvatar"];
|
private _roomAvatar?: Components["roomAvatar"];
|
||||||
|
private _notificationDecoration?: Components["notificationDecoration"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the components used by the API.
|
* Sets the components used by the API.
|
||||||
*
|
*
|
||||||
@@ -43,13 +47,13 @@ export class ElementWebBuiltinsApi implements BuiltinsApi {
|
|||||||
public setComponents(components: Components): void {
|
public setComponents(components: Components): void {
|
||||||
this._roomView = components.roomView;
|
this._roomView = components.roomView;
|
||||||
this._roomAvatar = components.roomAvatar;
|
this._roomAvatar = components.roomAvatar;
|
||||||
|
this._notificationDecoration = components.notificationDecoration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRoomViewComponent(): React.ComponentType<RoomViewPropsWithRoomId> {
|
public getRoomViewComponent(): React.ComponentType<RoomViewPropsWithRoomId> {
|
||||||
if (!this._roomView) {
|
if (!this._roomView) {
|
||||||
throw new Error("No RoomView component has been set");
|
throw new Error("No RoomView component has been set");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._roomView;
|
return this._roomView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +61,16 @@ export class ElementWebBuiltinsApi implements BuiltinsApi {
|
|||||||
if (!this._roomAvatar) {
|
if (!this._roomAvatar) {
|
||||||
throw new Error("No RoomAvatar component has been set");
|
throw new Error("No RoomAvatar component has been set");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._roomAvatar;
|
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 {
|
public renderRoomView(roomId: string, props?: RoomViewProps): React.ReactNode {
|
||||||
const Component = this.getRoomViewComponent();
|
const Component = this.getRoomViewComponent();
|
||||||
return <Component roomId={roomId} {...props} />;
|
return <Component roomId={roomId} {...props} />;
|
||||||
@@ -74,4 +84,13 @@ export class ElementWebBuiltinsApi implements BuiltinsApi {
|
|||||||
const Component = this.getRoomAvatarComponent();
|
const Component = this.getRoomAvatarComponent();
|
||||||
return <Component room={room} size={size} />;
|
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} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
};
|
||||||
+6
-1
@@ -33,6 +33,7 @@ import { UserFriendlyError } from "../languageHandler";
|
|||||||
import { ModuleApi } from "../modules/Api";
|
import { ModuleApi } from "../modules/Api";
|
||||||
import { RoomView } from "../components/structures/RoomView";
|
import { RoomView } from "../components/structures/RoomView";
|
||||||
import RoomAvatar from "../components/views/avatars/RoomAvatar";
|
import RoomAvatar from "../components/views/avatars/RoomAvatar";
|
||||||
|
import { ModuleNotificationDecoration } from "../modules/components/ModuleNotificationDecoration";
|
||||||
|
|
||||||
logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
|
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<MatrixChat>): Promise<ReactElement> {
|
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.
|
// 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.
|
// 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();
|
initRouting();
|
||||||
const platform = PlatformPeg.get();
|
const platform = PlatformPeg.get();
|
||||||
|
|||||||
@@ -44,10 +44,26 @@ describe("ElementWebBuiltinsApi", () => {
|
|||||||
expect(container).toHaveTextContent("50");
|
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", () => {
|
it("should throw error if called before components are set", () => {
|
||||||
stubClient();
|
stubClient();
|
||||||
const builtinsApi = new ElementWebBuiltinsApi();
|
const builtinsApi = new ElementWebBuiltinsApi();
|
||||||
expect(() => builtinsApi.renderRoomAvatar("!foo:m.org")).toThrow("No RoomAvatar component has been set");
|
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.renderRoomView("!foo:m.org")).toThrow("No RoomView component has been set");
|
||||||
|
expect(() => builtinsApi.renderNotificationDecoration("!foo:m.org")).toThrow(
|
||||||
|
"No NotificationDecoration component has been set",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -1559,10 +1559,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.16.1.tgz#28bdbde426051cc2a3228a36e7196e0a254569d3"
|
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==
|
integrity sha512-g3v/QFuNy8YVRGrKC5SxjIYvgBh6biOHgejhJT2Jk/yjOOUEuP0y2PBaADm+suPD9BB/Vk1jPxFk2uEIpEzhpA==
|
||||||
|
|
||||||
"@element-hq/element-web-module-api@1.7.0":
|
"@element-hq/element-web-module-api@1.8.0":
|
||||||
version "1.7.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.7.0.tgz#7657df25cc1e7075718af2c6ea8a4ebfaa9cfb2c"
|
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.8.0.tgz#95aa4ec22609cf0f4a7f24274473af0645a16f2a"
|
||||||
integrity sha512-WhiJTmdETK8vvaYExqyhQ9rtLjxBv9PprWr6dCa1/1VRFSkfFZRlzy2P08nHX2YXpRMTpXb39SLeleR1dgLzow==
|
integrity sha512-lMiDA9ubP3mZZupIMT8T3wS0riX30rYZj3pFpdP4cfZhkWZa3FJFStokAy5OnaHyENC7Px1cqkBGqilOWewY/A==
|
||||||
|
|
||||||
"@element-hq/element-web-playwright-common@^2.0.0":
|
"@element-hq/element-web-playwright-common@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -4151,7 +4151,7 @@
|
|||||||
classnames "^2.5.1"
|
classnames "^2.5.1"
|
||||||
vaul "^1.0.0"
|
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"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user