diff --git a/package.json b/package.json index 01abd04fa9..5c4f081b14 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", - "@vector-im/compound-design-tokens": "^0.0.7", + "@vector-im/compound-design-tokens": "^0.1.0", "@vector-im/compound-web": "0.8.1", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 306908af20..c19a649e48 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -50,6 +50,7 @@ import { formatCount } from "../../../utils/FormattingUtils"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { Linkify, topicToHtml } from "../../../HtmlUtils"; import PosthogTrackers from "../../../PosthogTrackers"; +import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton"; /** * A helper to transform a notification color to the what the Compound Icon Button @@ -215,6 +216,9 @@ export default function RoomHeader({ )} + {/* Renders nothing when room is not a video room */} + + = ({ room }) => { + const sdkContext = useContext(SDKContext); + + const isVideoRoom = calcIsVideoRoom(room); + + const notificationState = isVideoRoom ? sdkContext.roomNotificationStateStore.getRoomState(room) : undefined; + const notificationColor = useEventEmitterState( + notificationState, + NotificationStateEvents.Update, + () => notificationState?.color, + ); + + if (!isVideoRoom) { + return null; + } + + const displayUnreadIndicator = + !!notificationColor && + [NotificationColor.Bold, NotificationColor.Grey, NotificationColor.Red].includes(notificationColor); + + const onClick = (event: ButtonEvent): void => { + // stop event propagating up and triggering RoomHeader bar click + // which will open RoomSummary + event.stopPropagation(); + sdkContext.rightPanelStore.showOrHidePanel(RightPanelPhases.Timeline); + }; + + return ( + + + + + + ); +}; diff --git a/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx b/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx new file mode 100644 index 0000000000..3f8756028d --- /dev/null +++ b/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx @@ -0,0 +1,144 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { MockedObject } from "jest-mock"; +import { Room } from "matrix-js-sdk/src/matrix"; +import { fireEvent, render, screen } from "@testing-library/react"; + +import { VideoRoomChatButton } from "../../../../../src/components/views/rooms/RoomHeader/VideoRoomChatButton"; +import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext"; +import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; +import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState"; +import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor"; +import { NotificationStateEvents } from "../../../../../src/stores/notifications/NotificationState"; +import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases"; + +describe("", () => { + const roomId = "!room:server.org"; + let sdkContext!: SdkContextClass; + let rightPanelStore!: MockedObject; + + /** + * Create a room using mocked client + * And mock isElementVideoRoom + */ + const makeRoom = (isVideoRoom = true): Room => { + const room = new Room(roomId, sdkContext.client!, sdkContext.client!.getSafeUserId()); + jest.spyOn(room, "isElementVideoRoom").mockReturnValue(isVideoRoom); + // stub + jest.spyOn(room, "getPendingEvents").mockReturnValue([]); + return room; + }; + + const mockRoomNotificationState = (room: Room, color: NotificationColor): RoomNotificationState => { + const roomNotificationState = new RoomNotificationState(room); + + // @ts-ignore ugly mocking + roomNotificationState._color = color; + jest.spyOn(sdkContext.roomNotificationStateStore, "getRoomState").mockReturnValue(roomNotificationState); + return roomNotificationState; + }; + + const getComponent = (room: Room) => + render(, { + wrapper: ({ children }) => {children}, + }); + + beforeEach(() => { + const client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), + }); + rightPanelStore = { + showOrHidePanel: jest.fn(), + } as unknown as MockedObject; + sdkContext = new SdkContextClass(); + sdkContext.client = client; + jest.spyOn(sdkContext, "rightPanelStore", "get").mockReturnValue(rightPanelStore); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("does not render button when room is not a video room", () => { + const room = makeRoom(false); + getComponent(room); + + expect(screen.queryByLabelText("Chat")).not.toBeInTheDocument(); + }); + + it("renders button when room is a video room", () => { + const room = makeRoom(); + getComponent(room); + + expect(screen.getByLabelText("Chat")).toMatchSnapshot(); + }); + + it("toggles timeline in right panel on click", () => { + const room = makeRoom(); + getComponent(room); + + fireEvent.click(screen.getByLabelText("Chat")); + + expect(sdkContext.rightPanelStore.showOrHidePanel).toHaveBeenCalledWith(RightPanelPhases.Timeline); + }); + + it("renders button with an unread marker when room is unread", () => { + const room = makeRoom(); + mockRoomNotificationState(room, NotificationColor.Bold); + getComponent(room); + + // snapshot includes `data-indicator` attribute + expect(screen.getByLabelText("Chat")).toMatchSnapshot(); + expect(screen.getByLabelText("Chat").hasAttribute("data-indicator")).toBeTruthy(); + }); + + it("adds unread marker when room notification state changes to unread", () => { + const room = makeRoom(); + // start in read state + const notificationState = mockRoomNotificationState(room, NotificationColor.None); + getComponent(room); + + // no unread marker + expect(screen.getByLabelText("Chat").hasAttribute("data-indicator")).toBeFalsy(); + + // @ts-ignore ugly mocking + notificationState._color = NotificationColor.Red; + notificationState.emit(NotificationStateEvents.Update); + + // unread marker + expect(screen.getByLabelText("Chat").hasAttribute("data-indicator")).toBeTruthy(); + }); + + it("clears unread marker when room notification state changes to read", () => { + const room = makeRoom(); + // start in unread state + const notificationState = mockRoomNotificationState(room, NotificationColor.Red); + getComponent(room); + + // unread marker + expect(screen.getByLabelText("Chat").hasAttribute("data-indicator")).toBeTruthy(); + + // @ts-ignore ugly mocking + notificationState._color = NotificationColor.None; + notificationState.emit(NotificationStateEvents.Update); + + // unread marker cleared + expect(screen.getByLabelText("Chat").hasAttribute("data-indicator")).toBeFalsy(); + }); +}); diff --git a/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap new file mode 100644 index 0000000000..790dbf1eb8 --- /dev/null +++ b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders button when room is a video room 1`] = ` + +`; + +exports[` renders button with an unread marker when room is unread 1`] = ` + +`; diff --git a/yarn.lock b/yarn.lock index a727768770..a53f4062d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3037,10 +3037,10 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vector-im/compound-design-tokens@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-0.0.7.tgz#b0716dd4782dd95900491e45b003b58f93748024" - integrity sha512-RCQc6qr+s8cp4xKbNi/I3OL43uPCH+N4L9vYf0r+qwRy4WCKdI4QL0TBTV4bOo8hF49z8e+BgU5ZIu5TVQXNMQ== +"@vector-im/compound-design-tokens@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-0.1.0.tgz#1a574fba872ff93b1de8490f475e30b922cd02a2" + integrity sha512-vnDrd1CPPR7CwQLss/JnIE1ga6QwmCkhgBvXm1huMhCs7nIiqf90Sbgc0WugbHNaRXGEEhMVGrE69DaQIUcqOA== dependencies: svg2vectordrawable "^2.9.1"