From 6d99678ade67502c5b9c89d4bed0893ec33448cb Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:59:51 +0800 Subject: [PATCH 1/2] Redesign widget pip and move into shared component (#32654) * redesign widget pip and move into shared component * fix onBackClick handler * fix ci * Update README.md prepare -> prepack * add vm tests * Update WidgetPipView.stories.tsx * fix tests * playwright tests * fix test id * remove unused files (reappeared after rebase) * update storybook screenshot tests * update playwright tests * adjust padding * review * comment and docstring corrections * fix imports and `this.props` * fix double `complementary` item * add WidgetPipView tests and revmoe `setViewingRoom` from WidgetPipViewModelInterface. * add doc sting to `setViewingRoom` * Update RoomStatusBarView.test.tsx * fix copyright * Update RoomView-test.tsx.snap * revert accidental Copyright year changes * update snapshot RoomView-test --- .../playwright/e2e/voip/element-call.spec.ts | 25 ++- .../e2e/widgets/widget-pip-close.spec.ts | 4 +- apps/web/res/css/_components.pcss | 1 - .../css/components/views/pips/_WidgetPip.pcss | 73 ------- apps/web/res/css/views/rooms/_AppsDrawer.pcss | 12 +- .../structures/PictureInPictureDragger.tsx | 10 +- .../components/structures/PipContainer.tsx | 37 +++- .../src/components/views/pips/WidgetPip.tsx | 134 ------------ .../tabs/room/VoipRoomSettingsTab.tsx | 2 +- apps/web/src/hooks/useCall.ts | 5 - .../viewmodels/room/WidgetPipViewModel.tsx | 131 +++++++++++ .../structures/PipContainer-test.tsx | 40 +--- .../PictureInPictureDragger-test.tsx.snap | 8 +- .../__snapshots__/RoomView-test.tsx.snap | 4 +- .../test/viewmodels/room/WidgetPip-test.ts | 108 +++++++++ .../with-element-call-widget-mock-auto.png | Bin 0 -> 26241 bytes .../with-grey-widget-auto.png | Bin 0 -> 22371 bytes .../src/i18n/strings/en_EN.json | 1 + packages/shared-components/src/index.ts | 1 + .../RoomStatusBarView.module.css | 7 + .../room/WidgetPip/WidgetPipView.module.css | 35 +++ .../room/WidgetPip/WidgetPipView.stories.tsx | 89 ++++++++ .../src/room/WidgetPip/WidgetPipView.test.tsx | 51 +++++ .../src/room/WidgetPip/WidgetPipView.tsx | 109 ++++++++++ .../__snapshots__/WidgetPipView.test.tsx.snap | 205 ++++++++++++++++++ .../src/room/WidgetPip/index.ts | 8 + 26 files changed, 810 insertions(+), 290 deletions(-) delete mode 100644 apps/web/res/css/components/views/pips/_WidgetPip.pcss delete mode 100644 apps/web/src/components/views/pips/WidgetPip.tsx create mode 100644 apps/web/src/viewmodels/room/WidgetPipViewModel.tsx create mode 100644 apps/web/test/viewmodels/room/WidgetPip-test.ts create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room/WidgetPip/WidgetPipView.stories.tsx/with-element-call-widget-mock-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room/WidgetPip/WidgetPipView.stories.tsx/with-grey-widget-auto.png create mode 100644 packages/shared-components/src/room/WidgetPip/WidgetPipView.module.css create mode 100644 packages/shared-components/src/room/WidgetPip/WidgetPipView.stories.tsx create mode 100644 packages/shared-components/src/room/WidgetPip/WidgetPipView.test.tsx create mode 100644 packages/shared-components/src/room/WidgetPip/WidgetPipView.tsx create mode 100644 packages/shared-components/src/room/WidgetPip/__snapshots__/WidgetPipView.test.tsx.snap create mode 100644 packages/shared-components/src/room/WidgetPip/index.ts diff --git a/apps/web/playwright/e2e/voip/element-call.spec.ts b/apps/web/playwright/e2e/voip/element-call.spec.ts index 64f7a6f0f0..da956dfe15 100644 --- a/apps/web/playwright/e2e/voip/element-call.spec.ts +++ b/apps/web/playwright/e2e/voip/element-call.spec.ts @@ -508,15 +508,16 @@ test.describe("Element Call", () => { await openAndJoinCall(page); await app.viewRoomByName("OtherRoom"); - const pipContainer = page.locator(".mx_WidgetPip"); + const pipContainer = page.getByTestId("widget-pip-container"); // We should have a PiP container here. await expect(pipContainer).toBeVisible(); // Leave the call. - const overlay = page.locator(".mx_WidgetPip_overlay"); - await overlay.hover({ timeout: 2000 }); // Show the call footer. - await overlay.getByRole("button", { name: "Leave", exact: true }).click(); + const fakeWidget = page.locator('iframe[title="Element Call"]').contentFrame(); + + // await overlay.hover({ timeout: 2000 }); // Show the call footer. + await fakeWidget.getByRole("button", { name: "Close", exact: true }).click(); // PiP container goes. await expect(pipContainer).not.toBeVisible(); @@ -541,15 +542,14 @@ test.describe("Element Call", () => { await openAndJoinCall(page); await app.viewRoomByName("OtherRoom"); - const pipContainer = page.locator(".mx_WidgetPip"); + const pipContainer = page.getByTestId("widget-pip-container"); // We should have a PiP container here. await expect(pipContainer).toBeVisible(); // Leave the call. - const overlay = page.locator(".mx_WidgetPip_overlay"); - await overlay.hover({ timeout: 2000 }); // Show the call footer. - await overlay.getByRole("button", { name: "Leave", exact: true }).click(); + const fakeWidget = page.locator('iframe[title="Element Call"]').contentFrame(); + await fakeWidget.getByRole("button", { name: "Close", exact: true }).click(); // PiP container goes. await expect(pipContainer).not.toBeVisible(); @@ -578,15 +578,16 @@ test.describe("Element Call", () => { await openAndJoinCall(page, true); await app.viewRoomByName("OtherRoom"); - const pipContainer = page.locator(".mx_WidgetPip"); + const pipContainer = page.getByTestId("widget-pip-container"); // We should have a PiP container here. await expect(pipContainer).toBeVisible(); // Leave the call. - const overlay = page.locator(".mx_WidgetPip_overlay"); - await overlay.hover({ timeout: 2000 }); // Show the call footer. - await overlay.getByRole("button", { name: "Leave", exact: true }).click(); + const fakeWidget = page.locator('iframe[title="Element Call"]').contentFrame(); + + // await overlay.hover({ timeout: 2000 }); // Show the call footer. + await fakeWidget.getByRole("button", { name: "Close", exact: true }).click(); // PiP container goes. await expect(pipContainer).not.toBeVisible(); diff --git a/apps/web/playwright/e2e/widgets/widget-pip-close.spec.ts b/apps/web/playwright/e2e/widgets/widget-pip-close.spec.ts index e8163b5ee2..f2b01f5263 100644 --- a/apps/web/playwright/e2e/widgets/widget-pip-close.spec.ts +++ b/apps/web/playwright/e2e/widgets/widget-pip-close.spec.ts @@ -139,7 +139,7 @@ test.describe("Widget PIP", () => { ); // checks that pip window is opened - await expect(page.locator(".mx_WidgetPip")).toBeVisible(); + await expect(page.getByTestId("widget-pip-container")).toBeVisible(); // checks that widget is opened in pip const iframe = page.frameLocator(`iframe[title="${DEMO_WIDGET_NAME}"]`); @@ -155,7 +155,7 @@ test.describe("Widget PIP", () => { } // checks that pip window is closed - await expect(iframe.locator(".mx_WidgetPip")).not.toBeVisible(); + await expect(iframe.getByTestId("widget-pip-container")).not.toBeVisible(); }); } }); diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index a788baa032..339c4fd37c 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -34,7 +34,6 @@ @import "./components/views/location/_ZoomButtons.pcss"; @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; -@import "./components/views/pips/_WidgetPip.pcss"; @import "./components/views/polls/_PollOption.pcss"; @import "./components/views/settings/_AddRemoveThreepids.pcss"; @import "./components/views/settings/devices/_CurrentDeviceSection.pcss"; diff --git a/apps/web/res/css/components/views/pips/_WidgetPip.pcss b/apps/web/res/css/components/views/pips/_WidgetPip.pcss deleted file mode 100644 index b9ad791021..0000000000 --- a/apps/web/res/css/components/views/pips/_WidgetPip.pcss +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -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. -*/ - -$width: 320px; -$height: 220px; - -.mx_WidgetPip { - width: $width; - height: $height; -} - -.mx_WidgetPip_overlay { - width: $width; - height: $height; - position: absolute; - top: 0; - border-radius: 8px; - overflow: hidden; - color: $call-primary-content; - cursor: pointer; -} - -.mx_WidgetPip_header, -.mx_WidgetPip_footer { - position: absolute; - left: 0; - height: 60px; - width: 100%; - box-sizing: border-box; - transition: opacity ease 0.15s; -} - -.mx_WidgetPip_overlay:not(:hover) { - .mx_WidgetPip_header, - .mx_WidgetPip_footer { - opacity: 0; - } -} - -.mx_WidgetPip_header { - top: 0; - padding: $spacing-12; - display: flex; - font-size: $font-12px; - font-weight: var(--cpd-font-weight-semibold); - background: linear-gradient(rgb(0, 0, 0, 0.9), rgb(0, 0, 0, 0)); -} - -.mx_WidgetPip_backButton { - height: $spacing-24; - display: flex; - align-items: center; - gap: $spacing-12; - - > .mx_Icon { - color: $call-light-quaternary-content; - padding: 0; - } -} - -.mx_WidgetPip_footer { - bottom: 0; - padding: $spacing-12 $spacing-8; - display: flex; - justify-content: flex-end; - align-items: flex-end; - background: linear-gradient(rgb(0, 0, 0, 0), rgb(0, 0, 0, 0.9)); -} diff --git a/apps/web/res/css/views/rooms/_AppsDrawer.pcss b/apps/web/res/css/views/rooms/_AppsDrawer.pcss index 1ee13c4b8d..42b564d9f1 100644 --- a/apps/web/res/css/views/rooms/_AppsDrawer.pcss +++ b/apps/web/res/css/views/rooms/_AppsDrawer.pcss @@ -7,10 +7,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -:root { - --AppTile_mini-height: 220px; -} - .mx_AppsDrawer { --minWidth: 240px; /* TODO this should be 300px but that's too large */ @@ -168,11 +164,11 @@ Please see LICENSE files in the repository root for full details. .mx_AppTile_mini { width: 100%; + height: 100%; margin: 0; padding: 0; display: flex; flex-direction: column; - height: var(--AppTile_mini-height); } .mx_AppTile .mx_AppTile_persistedWrapper, @@ -276,8 +272,8 @@ Please see LICENSE files in the repository root for full details. &.mx_AppTileBody--large, &.mx_AppTileBody--mini { width: 100%; + height: 100%; overflow: hidden; - height: var(--AppTileBody-height); iframe { border: none; @@ -299,10 +295,6 @@ Please see LICENSE files in the repository root for full details. } } - &.mx_AppTileBody--mini { - --AppTileBody-height: var(--AppTile_mini-height); - } - &.mx_AppTileBody--loading { display: flex; flex-direction: column; diff --git a/apps/web/src/components/structures/PictureInPictureDragger.tsx b/apps/web/src/components/structures/PictureInPictureDragger.tsx index 057d600eef..2cadc59a7b 100644 --- a/apps/web/src/components/structures/PictureInPictureDragger.tsx +++ b/apps/web/src/components/structures/PictureInPictureDragger.tsx @@ -18,10 +18,10 @@ const MOVING_AMT = 0.2; const SNAPPING_AMT = 0.1; const PADDING = { - top: 58, - bottom: 58, - left: 76, - right: 8, + top: 80, + bottom: 87, + left: 84, + right: 16, }; /** @@ -53,7 +53,7 @@ export default class PictureInPictureDragger extends React.Component { private initX = 0; private initY = 0; private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH; - private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT; + private desiredTranslationY = PADDING.top; private translationX = this.desiredTranslationX; private translationY = this.desiredTranslationY; private mouseHeld = false; diff --git a/apps/web/src/components/structures/PipContainer.tsx b/apps/web/src/components/structures/PipContainer.tsx index 9dbb6daef7..2eab9efc93 100644 --- a/apps/web/src/components/structures/PipContainer.tsx +++ b/apps/web/src/components/structures/PipContainer.tsx @@ -6,9 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type RefObject, type ReactNode, useRef } from "react"; +import React, { type RefObject, type ReactNode, useRef, useEffect } from "react"; import { CallEvent, CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; +import { useCreateAutoDisposedViewModel, WidgetPipView } from "@element-hq/web-shared-components"; import LegacyCallView from "../views/voip/LegacyCallView"; import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; @@ -21,7 +22,8 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWi import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { SdkContextClass } from "../../contexts/SDKContext"; -import { WidgetPip } from "../views/pips/WidgetPip"; +import RoomAvatar from "../views/avatars/RoomAvatar"; +import { WidgetPipViewModel, type Props as WidgetPipViewModelProps } from "../../viewmodels/room/WidgetPipViewModel"; const SHOW_CALL_IN_STATES = [ CallState.Connected, @@ -46,7 +48,7 @@ interface IState { // they belong to secondaryCall: MatrixCall; - // widget candidate to be displayed in the pip view. + // Widget candidate to be displayed in the PiP view. persistentWidgetId: string | null; persistentRoomId: string | null; showWidgetInPip: boolean; @@ -251,7 +253,7 @@ class PipContainerInner extends React.Component { if (this.state.showWidgetInPip && this.state.persistentWidgetId) { pipContent.push(({ onStartMoving }) => ( - { return ; }; + +type Props = { viewingRoom: boolean } & WidgetPipViewModelProps; + +/** + * A wrapper for the WidgetPipView component. + * + * This exposes the new shared WidgetPipView with the same API as before and how + * it is used in the PipContainerInner component. + * @param props The same props the legacy WidgetPip was using. + * @returns + */ +const WidgetPipWrappedView: React.FC = (props: Props) => { + const vm = useCreateAutoDisposedViewModel(() => new WidgetPipViewModel(props)); + + useEffect(() => { + // Use an effect to update viewingRoom. It is not required in the view but only in the view model. + vm.setViewingRoom(props.viewingRoom); + }, [vm, props.viewingRoom]); + + return ( + } + /> + ); +}; diff --git a/apps/web/src/components/views/pips/WidgetPip.tsx b/apps/web/src/components/views/pips/WidgetPip.tsx deleted file mode 100644 index ed8cdb7e95..0000000000 --- a/apps/web/src/components/views/pips/WidgetPip.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -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, { type FC, type RefObject, useCallback, useMemo } from "react"; -import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; -import { ArrowLeftIcon, EndCallIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -import PersistentApp from "../elements/PersistentApp"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Action } from "../../../dispatcher/actions"; -import { useCallForWidget } from "../../../hooks/useCall"; -import WidgetStore from "../../../stores/WidgetStore"; -import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; -import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import Toolbar from "../../../accessibility/Toolbar"; -import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex"; -import { _t } from "../../../languageHandler"; -import { WidgetType } from "../../../widgets/WidgetType"; -import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; -import WidgetUtils from "../../../utils/WidgetUtils"; -import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions"; -import { type ButtonEvent } from "../elements/AccessibleButton"; - -interface Props { - widgetId: string; - room: Room; - viewingRoom: boolean; - onStartMoving: (e: React.MouseEvent) => void; - movePersistedElement: RefObject<(() => void) | null>; -} - -/** - * A picture-in-picture view for a widget. Additional controls are shown if the - * widget is a call of some sort. - */ -export const WidgetPip: FC = ({ widgetId, room, viewingRoom, onStartMoving, movePersistedElement }) => { - const widget = useMemo( - () => WidgetStore.instance.getApps(room.roomId).find((app) => app.id === widgetId)!, - [room, widgetId], - ); - - const roomName = useTypedEventEmitterState( - room, - RoomEvent.Name, - useCallback(() => room.name, [room]), - ); - - const call = useCallForWidget(widgetId, room.roomId); - - const onBackClick = useCallback( - (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - if (call !== null) { - defaultDispatcher.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - view_call: true, - metricsTrigger: "WebFloatingCallWindow", - }); - } else if (viewingRoom) { - WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Center); - } else { - defaultDispatcher.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: "WebFloatingCallWindow", - }); - } - }, - [room, call, widget, viewingRoom], - ); - - const onLeaveClick = useCallback( - (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - if (call !== null) { - call.disconnect().catch((e) => console.error("Failed to leave call", e)); - } else { - // Assumed to be a Jitsi widget - WidgetMessagingStore.instance - .getMessagingForUid(WidgetUtils.getWidgetUid(widget)) - ?.widgetApi?.transport.send(ElementWidgetActions.HangupCall, {}) - .catch((e) => console.error("Failed to leave Jitsi", e)); - } - }, - [call, widget], - ); - - return ( -
- -
- - - - {roomName} - - - {(call !== null || WidgetType.JITSI.matches(widget?.type)) && ( - - - - - - )} -
-
-
- ); -}; diff --git a/apps/web/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/apps/web/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 6c28105060..a5705694e9 100644 --- a/apps/web/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/apps/web/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -76,7 +76,7 @@ const ElementCallSwitch: React.FC = ({ room }) => { return ( { return call; }; -export const useCallForWidget = (widgetId: string, roomId: string): Call | null => { - const call = useCall(roomId); - return call?.widget.id === widgetId ? call : null; -}; - export const useConnectionState = (call: Call | null): ConnectionState => useTypedEventEmitterState( call ?? undefined, diff --git a/apps/web/src/viewmodels/room/WidgetPipViewModel.tsx b/apps/web/src/viewmodels/room/WidgetPipViewModel.tsx new file mode 100644 index 0000000000..e3deb31c4d --- /dev/null +++ b/apps/web/src/viewmodels/room/WidgetPipViewModel.tsx @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2026 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 { + BaseViewModel, + type WidgetPipViewSnapshot, + type WidgetPipViewModel as WidgetPipViewModelInterface, +} from "@element-hq/web-shared-components"; +import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +import React from "react"; + +import type { RefObject, FC } from "react"; +import { Action } from "../../dispatcher/actions"; +import WidgetStore, { type IApp } from "../../stores/WidgetStore"; +import { CallStore, CallStoreEvent } from "../../stores/CallStore"; +import { type Call } from "../../models/Call"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; +import PersistentApp from "../../components/views/elements/PersistentApp"; + +export interface Props { + /** + * The widgetId this widget PiP view is showing. + */ + widgetId: string; + /** + * The room this widget PiP view model is associated with. + */ + room: Room; + /** + * A callback which is called when a mouse event (most likely mouse down) occurs at the start of moving the PiP around. + */ + onStartMoving: (ev: React.MouseEvent) => void; + /** + * This callback ref will be used by the ViewModel once the view is moving. + * Widgets might be implemented with a top-layer DOM tree path containing the widget iframe. + * This allows moving the iframe around (PiP/in-room) without remounting it. + * This callback allows any `PersistentApp` view/component to know when to update the iframe position of the widget. + */ + movePersistedElement: RefObject<(() => void) | null>; +} + +export class WidgetPipViewModel + extends BaseViewModel + implements WidgetPipViewModelInterface +{ + /** The widget this view model uses for the PipView */ + private readonly widget: IApp; + /** + * The call associated with the widget (if the widget is a call widget) + * For non-call widgets, this will be `null`. + */ + private call: Call | null; + /** If the user is currently viewing the room associated with the PiP view (`this.props.room`) */ + private viewingRoom?: boolean; + + public constructor(props: Props) { + super(props, { widgetId: props.widgetId, roomName: props.room.name, roomId: props.room.roomId }); + this.widget = WidgetStore.instance.getApps(props.room.roomId).find((app) => app.id === this.props.widgetId)!; + this.call = CallStore.instance.getCall(props.room.roomId) ?? null; + this.onStartMoving = props.onStartMoving; + + this.disposables.trackListener(props.room, RoomEvent.Name, this.onRoomName); + this.disposables.trackListener(CallStore.instance, CallStoreEvent.Call, this.onCallChange); + } + + public onStartMoving: (ev: React.MouseEvent) => void; + + /** + * The view model needs to know if the room is currently being viewed. + * @param viewing Whether we are currently viewing the room. + */ + public setViewingRoom(viewing: boolean): void { + this.viewingRoom = viewing; + } + + public onBackClick(ev: React.MouseEvent): void { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.call !== null) { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: this.props.room.roomId, + view_call: true, + metricsTrigger: "WebFloatingCallWindow", + }); + } else if (this.viewingRoom) { + WidgetLayoutStore.instance.moveToContainer(this.props.room, this.widget, Container.Center); + } else { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: this.props.room.roomId, + metricsTrigger: "WebFloatingCallWindow", + }); + } + } + + /** + * The component to render as the persistent app by the WidgetPipView. + * @param props A copy of the `PersistentApp` component's props. + * @returns + */ + public persistentAppComponent: FC< + Pick, "persistentWidgetId" | "persistentRoomId"> + > = (props) => { + return ( + + ); + }; + + private readonly onRoomName = (): void => { + this.snapshot.merge({ roomName: this.props.room.name }); + }; + + private readonly onCallChange = (...args: unknown[]): void => { + const [call, forRoomId] = args as [Call | null, string]; + if (forRoomId === this.props.room.roomId) { + this.call = call?.widget.id === this.props.widgetId ? call : null; + } + }; +} diff --git a/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx b/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx index 35966d6d02..be30707f2e 100644 --- a/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx +++ b/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx @@ -17,7 +17,7 @@ import { RoomStateEvent, type RoomMember, } from "matrix-js-sdk/src/matrix"; -import { Widget, type ClientWidgetApi } from "matrix-widget-api"; +import { Widget } from "matrix-widget-api"; import { useMockedCalls, @@ -47,7 +47,6 @@ import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/Wid import WidgetStore from "../../../../src/stores/WidgetStore"; import { WidgetType } from "../../../../src/widgets/WidgetType"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; -import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions"; import { type WidgetMessaging } from "../../../../src/stores/widgets/WidgetMessaging"; jest.mock("../../../../src/stores/OwnProfileStore", () => ({ @@ -194,7 +193,7 @@ describe("PipContainer", () => { expect(screen.queryByRole("complementary")).toBeNull(); }); - it("shows an active call with back and leave buttons", async () => { + it("shows an active call with back buttons", async () => { renderPip(); await withCall(async (call) => { @@ -211,11 +210,6 @@ describe("PipContainer", () => { metricsTrigger: expect.any(String), }); defaultDispatcher.unregister(dispatcherRef); - - // The leave button should disconnect from the call - const disconnectSpy = jest.spyOn(call, "disconnect"); - await user.click(screen.getByRole("button", { name: "Leave" })); - expect(disconnectSpy).toHaveBeenCalled(); }); }); @@ -252,16 +246,7 @@ describe("PipContainer", () => { mockPlatformPeg({ supportsJitsiScreensharing: () => true }); setUpRoomViewStore(); viewRoom(room2.roomId); - const widget = WidgetStore.instance.addVirtualWidget( - { - id: "1", - creatorUserId: "@alice:example.org", - type: WidgetType.JITSI.preferred, - url: "https://meet.example.org", - name: "Jitsi example", - }, - room.roomId, - ); + renderPip(); await withWidget(async () => { @@ -277,25 +262,6 @@ describe("PipContainer", () => { metricsTrigger: expect.any(String), }); defaultDispatcher.unregister(dispatcherRef); - - // The leave button should hangup the call - const sendSpy = jest - .fn< - ReturnType, - Parameters - >() - .mockResolvedValue({}); - const mockMessaging = { - on: () => {}, - off: () => {}, - stop: () => {}, - widgetApi: { - transport: { send: sendSpy }, - }, - } as unknown as WidgetMessaging; - WidgetMessagingStore.instance.storeMessaging(new Widget(widget), room.roomId, mockMessaging); - await user.click(screen.getByRole("button", { name: "Leave" })); - expect(sendSpy).toHaveBeenCalledWith(ElementWidgetActions.HangupCall, {}); }); WidgetStore.instance.removeVirtualWidget("1", room.roomId); diff --git a/apps/web/test/unit-tests/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap b/apps/web/test/unit-tests/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap index 3d214ab98c..9020570c2d 100644 --- a/apps/web/test/unit-tests/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap +++ b/apps/web/test/unit-tests/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap @@ -3,7 +3,7 @@ exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 and 2 should render both contents 1`] = `