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
This commit is contained in:
@@ -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<IProps> {
|
||||
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;
|
||||
|
||||
@@ -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<IProps, IState> {
|
||||
|
||||
if (this.state.showWidgetInPip && this.state.persistentWidgetId) {
|
||||
pipContent.push(({ onStartMoving }) => (
|
||||
<WidgetPip
|
||||
<WidgetPipWrappedView
|
||||
key="widget-pip"
|
||||
widgetId={this.state.persistentWidgetId!}
|
||||
room={MatrixClientPeg.safeGet().getRoom(this.state.persistentRoomId ?? undefined)!}
|
||||
@@ -284,3 +286,30 @@ export const PipContainer: React.FC = () => {
|
||||
|
||||
return <PipContainerInner movePersistedElement={movePersistedElement} />;
|
||||
};
|
||||
|
||||
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: 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 (
|
||||
<WidgetPipView
|
||||
vm={vm}
|
||||
// Props only used in the view and not the view model get passed directly.
|
||||
RoomAvatar={({ size }) => <RoomAvatar size={size} room={props.room} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<Element, 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<Props> = ({ 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<ViewRoomPayload>({
|
||||
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<ViewRoomPayload>({
|
||||
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 (
|
||||
<div className="mx_WidgetPip" onMouseDown={onStartMoving} onClick={onBackClick}>
|
||||
<PersistentApp
|
||||
persistentWidgetId={widgetId}
|
||||
persistentRoomId={room.roomId}
|
||||
pointerEvents="none"
|
||||
movePersistedElement={movePersistedElement}
|
||||
>
|
||||
<div onMouseDown={onStartMoving} className="mx_WidgetPip_overlay">
|
||||
<Toolbar className="mx_WidgetPip_header">
|
||||
<RovingAccessibleButton
|
||||
onClick={onBackClick}
|
||||
className="mx_WidgetPip_backButton"
|
||||
aria-label={_t("action|back")}
|
||||
>
|
||||
<ArrowLeftIcon className="mx_Icon mx_Icon_16" />
|
||||
{roomName}
|
||||
</RovingAccessibleButton>
|
||||
</Toolbar>
|
||||
{(call !== null || WidgetType.JITSI.matches(widget?.type)) && (
|
||||
<Toolbar className="mx_WidgetPip_footer">
|
||||
<RovingAccessibleButton
|
||||
onClick={onLeaveClick}
|
||||
title={_t("action|leave")}
|
||||
aria-label={_t("action|leave")}
|
||||
placement="top"
|
||||
>
|
||||
<EndCallIcon className="mx_Icon mx_Icon_24" />
|
||||
</RovingAccessibleButton>
|
||||
</Toolbar>
|
||||
)}
|
||||
</div>
|
||||
</PersistentApp>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -76,7 +76,7 @@ const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ room }) => {
|
||||
return (
|
||||
<SettingsToggleInput
|
||||
name="element-call-switch"
|
||||
data-test-id="element-call-switch"
|
||||
data-testid="element-call-switch"
|
||||
label={_t("room_settings|voip|enable_element_call_label", { brand })}
|
||||
helpMessage={_t("room_settings|voip|enable_element_call_caption", {
|
||||
brand,
|
||||
|
||||
Reference in New Issue
Block a user