Redesign the picture-in-picture window (#9800)

* Remove unnecessary PipContainer component

* Redesign the picture-in-picture window

* Add a hover effect to the controls

* Clarify that WidgetPip has call-specific behavior
This commit is contained in:
Robin 2023-01-03 23:44:38 -05:00 committed by GitHub
parent 6b155620e4
commit cb1af0d3de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 491 additions and 328 deletions

View file

@ -41,7 +41,6 @@ import { DefaultTagID } from "../../stores/room-list/models";
import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel from "./LeftPanel";
import PipContainer from "../views/voip/PipContainer";
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer";
@ -71,6 +70,7 @@ import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload
import { IConfigOptions } from "../../IConfigOptions";
import LeftPanelLiveShareWarning from "../views/beacon/LeftPanelLiveShareWarning";
import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage";
import { PipContainer } from "./PipContainer";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.

View file

@ -16,9 +16,9 @@ limitations under the License.
import React, { createRef } from "react";
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
import { lerp } from "../../../utils/AnimationUtils";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
import { lerp } from "../../utils/AnimationUtils";
import { MarkedExecution } from "../../utils/MarkedExecution";
const PIP_VIEW_WIDTH = 336;
const PIP_VIEW_HEIGHT = 232;
@ -65,12 +65,20 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT;
private translationX = this.desiredTranslationX;
private translationY = this.desiredTranslationY;
private moving = false;
private scheduledUpdate = new MarkedExecution(
private mouseHeld = false;
private scheduledUpdate: MarkedExecution = new MarkedExecution(
() => this.animationCallback(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
private _moving = false;
public get moving(): boolean {
return this._moving;
}
private set moving(value: boolean) {
this._moving = value;
}
public componentDidMount() {
document.addEventListener("mousemove", this.onMoving);
document.addEventListener("mouseup", this.onEndMoving);
@ -183,26 +191,47 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
event.preventDefault();
event.stopPropagation();
this.moving = true;
this.initX = event.pageX - this.desiredTranslationX;
this.initY = event.pageY - this.desiredTranslationY;
this.scheduledUpdate.mark();
this.mouseHeld = true;
};
private onMoving = (event: React.MouseEvent | MouseEvent) => {
if (!this.moving) return;
private onMoving = (event: MouseEvent) => {
if (!this.mouseHeld) return;
event.preventDefault();
event.stopPropagation();
if (!this.moving) {
this.moving = true;
this.initX = event.pageX - this.desiredTranslationX;
this.initY = event.pageY - this.desiredTranslationY;
this.scheduledUpdate.mark();
}
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
};
private onEndMoving = () => {
this.moving = false;
private onEndMoving = (event: MouseEvent) => {
if (!this.mouseHeld) return;
event.preventDefault();
event.stopPropagation();
this.mouseHeld = false;
// Delaying this to the next event loop tick is necessary for click
// event cancellation to work
setImmediate(() => (this.moving = false));
this.snap(true);
};
private onClickCapture = (event: React.MouseEvent) => {
// To prevent mouse up events during dragging from being double-counted
// as clicks, we cancel clicks before they ever reach the target
if (this.moving) {
event.preventDefault();
event.stopPropagation();
}
};
public render() {
const style = {
transform: `translateX(${this.translationX}px) translateY(${this.translationY}px)`,
@ -220,6 +249,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
className={this.props.className}
style={style}
ref={this.callViewWrapper}
onClickCapture={this.onClickCapture}
onDoubleClick={this.props.onDoubleClick}
>
{children}

View file

@ -14,28 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, useContext } from "react";
import React, { MutableRefObject, useContext, useRef } from "react";
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { Optional } from "matrix-events-sdk";
import LegacyCallView from "./LegacyCallView";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler";
import PersistentApp from "../elements/PersistentApp";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import LegacyCallView from "../views/voip/LegacyCallView";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import PictureInPictureDragger, { CreatePipChildren } from "./PictureInPictureDragger";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import LegacyCallViewHeader from "./LegacyCallView/LegacyCallViewHeader";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../../stores/ActiveWidgetStore";
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
import { CallStore } from "../../../stores/CallStore";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
import {
useCurrentVoiceBroadcastPreRecording,
useCurrentVoiceBroadcastRecording,
@ -46,8 +40,9 @@ import {
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
VoiceBroadcastSmallPlaybackBody,
} from "../../../voice-broadcast";
import { useCurrentVoiceBroadcastPlayback } from "../../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
} from "../../voice-broadcast";
import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
import { WidgetPip } from "../views/pips/WidgetPip";
const SHOW_CALL_IN_STATES = [
CallState.Connected,
@ -59,9 +54,10 @@ const SHOW_CALL_IN_STATES = [
];
interface IProps {
voiceBroadcastRecording?: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording?: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback?: Optional<VoiceBroadcastPlayback>;
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
movePersistedElement: MutableRefObject<(() => void) | undefined>;
}
interface IState {
@ -78,20 +74,8 @@ interface IState {
persistentWidgetId: string;
persistentRoomId: string;
showWidgetInPip: boolean;
moving: boolean;
}
const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room | null, IApp | null] => {
if (!widgetId) return [null, null];
if (!roomId) return [null, null];
const room = MatrixClientPeg.get().getRoom(roomId);
const app = WidgetStore.instance.getApps(roomId).find((app) => app.id === widgetId);
return [room, app || null];
};
// Splits a list of calls into one 'primary' one and a list
// (which should be a single element) of other calls.
// The primary will be the one not on hold, or an arbitrary one
@ -128,16 +112,12 @@ function getPrimarySecondaryCallsForPip(roomId: Optional<string>): [MatrixCall |
}
/**
* PipView shows a small version of the LegacyCallView or a sticky widget hovering over the UI in 'picture-in-picture'
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
* PipContainer shows a small version of the LegacyCallView or a sticky widget hovering over the UI in
* 'picture-in-picture' (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
* and all widgets that are active but not shown in any other possible container.
*/
class PipView extends React.Component<IProps, IState> {
// The cast is not so great, but solves the typing issue for the moment.
// Proper solution: use useRef (requires the component to be refactored to a functional component).
private movePersistedElement = createRef<() => void>() as React.MutableRefObject<() => void>;
class PipContainerInner extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
@ -146,7 +126,6 @@ class PipView extends React.Component<IProps, IState> {
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
this.state = {
moving: false,
viewedRoomId: roomId || undefined,
primaryCall: primaryCall || null,
secondaryCall: secondaryCalls[0],
@ -168,7 +147,6 @@ class PipView extends React.Component<IProps, IState> {
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
document.addEventListener("mouseup", this.onEndMoving.bind(this));
}
public componentWillUnmount() {
@ -184,18 +162,9 @@ class PipView extends React.Component<IProps, IState> {
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
document.removeEventListener("mouseup", this.onEndMoving.bind(this));
}
private onStartMoving() {
this.setState({ moving: true });
}
private onEndMoving() {
this.setState({ moving: false });
}
private onMove = () => this.movePersistedElement.current?.();
private onMove = () => this.props.movePersistedElement.current?.();
private onRoomViewStoreUpdate = () => {
const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
@ -265,53 +234,6 @@ class PipView extends React.Component<IProps, IState> {
}
};
private onMaximize = (): void => {
const widgetId = this.state.persistentWidgetId;
const roomId = this.state.persistentRoomId;
if (this.state.showWidgetInPip && widgetId && roomId) {
const [room, app] = getRoomAndAppForWidget(widgetId, roomId);
if (room && app) {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
return;
}
}
dis.dispatch({
action: "video_fullscreen",
fullscreen: true,
});
};
private onPin = (): void => {
if (!this.state.showWidgetInPip) return;
const [room, app] = getRoomAndAppForWidget(this.state.persistentWidgetId, this.state.persistentRoomId);
if (room && app) {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
}
};
private onExpand = (): void => {
const widgetId = this.state.persistentWidgetId;
if (!widgetId || !this.state.showWidgetInPip) return;
dis.dispatch({
action: Action.ViewRoom,
room_id: this.state.persistentRoomId,
});
};
private onViewCall = (): void =>
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.state.persistentRoomId,
view_call: true,
metricsTrigger: undefined,
});
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
public updateShowWidgetInPip(
persistentWidgetId = this.state.persistentWidgetId,
@ -398,36 +320,14 @@ class PipView extends React.Component<IProps, IState> {
}
if (this.state.showWidgetInPip) {
const pipViewClasses = classNames({
mx_LegacyCallView: true,
mx_LegacyCallView_pip: pipMode,
mx_LegacyCallView_large: !pipMode,
});
const roomId = this.state.persistentRoomId;
const roomForWidget = MatrixClientPeg.get().getRoom(roomId)!;
const viewingCallRoom = this.state.viewedRoomId === roomId;
const isCall = CallStore.instance.getActiveCall(roomId) !== null;
pipContent.push(({ onStartMoving }) => (
<div className={pipViewClasses}>
<LegacyCallViewHeader
onPipMouseDown={(event) => {
onStartMoving?.(event);
this.onStartMoving.bind(this)();
}}
pipMode={pipMode}
callRooms={[roomForWidget]}
onExpand={!isCall && !viewingCallRoom ? this.onExpand : undefined}
onPin={!isCall && viewingCallRoom ? this.onPin : undefined}
onMaximize={isCall ? this.onViewCall : viewingCallRoom ? this.onMaximize : undefined}
/>
<PersistentApp
persistentWidgetId={this.state.persistentWidgetId}
persistentRoomId={roomId}
pointerEvents={this.state.moving ? "none" : undefined}
movePersistedElement={this.movePersistedElement}
/>
</div>
<WidgetPip
widgetId={this.state.persistentWidgetId}
room={MatrixClientPeg.get().getRoom(this.state.persistentRoomId)!}
viewingRoom={this.state.viewedRoomId === this.state.persistentRoomId}
onStartMoving={onStartMoving}
movePersistedElement={this.props.movePersistedElement}
/>
));
}
@ -448,7 +348,7 @@ class PipView extends React.Component<IProps, IState> {
}
}
const PipViewHOC: React.FC<IProps> = (props) => {
export const PipContainer: React.FC = () => {
const sdkContext = useContext(SDKContext);
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
@ -459,14 +359,14 @@ const PipViewHOC: React.FC<IProps> = (props) => {
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
const movePersistedElement = useRef<() => void>();
return (
<PipView
<PipContainerInner
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
voiceBroadcastRecording={currentVoiceBroadcastRecording}
{...props}
movePersistedElement={movePersistedElement}
/>
);
};
export default PipViewHOC;

View file

@ -85,7 +85,7 @@ interface IProps {
widgetPageTitle?: string;
showLayoutButtons?: boolean;
// Handle to manually notify the PersistedElement that it needs to move
movePersistedElement?: MutableRefObject<() => void>;
movePersistedElement?: MutableRefObject<(() => void) | undefined>;
}
interface IState {

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { MutableRefObject } from "react";
import ReactDOM from "react-dom";
import { throttle } from "lodash";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import dis from "../../../dispatcher/dispatcher";
@ -58,7 +57,7 @@ interface IProps {
style?: React.StyleHTMLAttributes<HTMLDivElement>;
// Handle to manually notify this PersistedElement that it needs to move
moveRef?: MutableRefObject<() => void>;
moveRef?: MutableRefObject<(() => void) | undefined>;
}
/**
@ -177,24 +176,20 @@ export default class PersistedElement extends React.Component<IProps> {
child.style.display = visible ? "block" : "none";
}
private updateChildPosition = throttle(
(child: HTMLDivElement, parent: HTMLDivElement): void => {
if (!child || !parent) return;
private updateChildPosition(child: HTMLDivElement, parent: HTMLDivElement): void {
if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, {
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
position: "absolute",
top: "0",
left: "0",
transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`,
width: parentRect.width + "px",
height: parentRect.height + "px",
});
},
16,
{ trailing: true, leading: true },
);
const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, {
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
position: "absolute",
top: "0",
left: "0",
transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`,
width: parentRect.width + "px",
height: parentRect.height + "px",
});
}
public render(): JSX.Element {
return <div ref={this.collectChildContainer} />;

View file

@ -27,7 +27,7 @@ interface IProps {
persistentWidgetId: string;
persistentRoomId: string;
pointerEvents?: string;
movePersistedElement: MutableRefObject<() => void>;
movePersistedElement: MutableRefObject<(() => void) | undefined>;
}
export default class PersistentApp extends React.Component<IProps> {

View file

@ -0,0 +1,140 @@
/*
Copyright 2022 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, { FC, MutableRefObject, useCallback, useMemo } from "react";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import PersistentApp from "../elements/PersistentApp";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { 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, RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import { Icon as BackIcon } from "../../../../res/img/element-icons/back.svg";
import { Icon as HangupIcon } from "../../../../res/img/element-icons/call/hangup.svg";
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 { Alignment } from "../elements/Tooltip";
interface Props {
widgetId: string;
room: Room;
viewingRoom: boolean;
onStartMoving: (e: React.MouseEvent<Element, MouseEvent>) => void;
movePersistedElement: MutableRefObject<(() => void) | undefined>;
}
/**
* 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) => {
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) => {
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))
?.transport.send(ElementWidgetActions.HangupCall, {})
.catch((e) => console.error("Failed to leave Jitsi", e));
}
},
[call, widget],
);
return (
<div className="mx_WidgetPip" onMouseDown={onStartMoving} onClick={onBackClick}>
<Toolbar className="mx_WidgetPip_header">
<RovingAccessibleButton
onClick={onBackClick}
className="mx_WidgetPip_backButton"
aria-label={_t("Back")}
>
<BackIcon className="mx_Icon mx_Icon_16" />
{roomName}
</RovingAccessibleButton>
</Toolbar>
<PersistentApp
persistentWidgetId={widgetId}
persistentRoomId={room.roomId}
pointerEvents="none"
movePersistedElement={movePersistedElement}
/>
{(call !== null || WidgetType.JITSI.matches(widget.type)) && (
<Toolbar className="mx_WidgetPip_footer">
<RovingAccessibleTooltipButton
onClick={onLeaveClick}
tooltip={_t("Leave")}
aria-label={_t("Leave")}
alignment={Alignment.Top}
>
<HangupIcon className="mx_Icon mx_Icon_24" />
</RovingAccessibleTooltipButton>
</Toolbar>
)}
</div>
);
};

View file

@ -1,34 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 PipView from "./PipView";
interface IProps {}
interface IState {}
export default class PiPContainer extends React.PureComponent<IProps, IState> {
public render() {
return (
<div className="mx_PiPContainer">
<PipView />
</div>
);
}
}

View file

@ -33,6 +33,11 @@ export const useCall = (roomId: string): Call | null => {
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): ConnectionState =>
useTypedEventEmitterState(
call,