New group call experience: Room header and PiP designs (#9351)

* Update our cancel icon

The cancel icon we're using in the app has drifted out of sync with the ones used in our designs. We also had two identical-looking icons, so this consolidates them into one.

I've simultaneously updated our chevron icons, since in the case of the 'jump to unread' timeline button, it became clear that the weight of the new close icon did not match the thinner chevron.

* Don't squish bottom/top-aligned tooltips near the edge of the screen

* Close the timeline panel when returning to the fullscreen timeline view

* Add layout switching capabilities to ElementCall

* Bring the room header in line with the group call designs

* Bring the PiP header in line with the group call designs

* Fix lints

* Clarify tooltip CSS calculations

* Test PipView

* Expand RoomHeader test coverage

* Test PipView more
This commit is contained in:
Robin 2022-10-06 22:27:28 -04:00 committed by GitHub
parent 9a3ae2398e
commit 06dbea6255
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 845 additions and 220 deletions

View file

@ -493,7 +493,6 @@ export class EmailIdentityAuthEntry extends
? _t("Resent!")
: _t("Resend")}
alignment={Alignment.Right}
tooltipClassName="mx_Tooltip_noMargin"
onHideTooltip={this.state.requested
? () => this.setState({ requested: false })
: undefined}

View file

@ -149,18 +149,24 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
break;
case Alignment.Top:
style.top = baseTop - spacing;
style.left = horizontalCenter;
style.transform = "translate(-50%, -100%)";
// Attempt to center the tooltip on the element while clamping
// its horizontal translation to keep it on screen
// eslint-disable-next-line max-len
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))), -100%)`;
break;
case Alignment.Bottom:
style.top = baseTop + parentBox.height + spacing;
style.left = horizontalCenter;
style.transform = "translate(-50%)";
// Attempt to center the tooltip on the element while clamping
// its horizontal translation to keep it on screen
// eslint-disable-next-line max-len
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
break;
case Alignment.InnerBottom:
style.top = baseTop + parentBox.height - 50;
style.left = horizontalCenter;
style.transform = "translate(-50%)";
// Attempt to center the tooltip on the element while clamping
// its horizontal translation to keep it on screen
// eslint-disable-next-line max-len
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
break;
case Alignment.TopRight:
style.top = baseTop - spacing;

View file

@ -23,6 +23,7 @@ import classNames from 'classnames';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ButtonEvent } from "../elements/AccessibleButton";
import { Alignment } from "../elements/Tooltip";
interface IProps {
// Whether this button is highlighted
@ -54,6 +55,7 @@ export default class HeaderButton extends React.Component<IProps> {
aria-selected={isHighlighted}
role="tab"
title={title}
alignment={Alignment.Bottom}
className={classes}
onClick={onClick}
/>;

View file

@ -282,7 +282,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
<HeaderButton
key="roomSummaryButton"
name="roomSummaryButton"
title={_t('Room Info')}
title={_t('Room info')}
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
onClick={this.onRoomSummaryClicked}
/>,

View file

@ -20,7 +20,7 @@ import classNames from 'classnames';
import { _t, _td } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import Tooltip from "../elements/Tooltip";
import Tooltip, { Alignment } from "../elements/Tooltip";
import { E2EStatus } from "../../../utils/ShieldUtils";
export enum E2EState {
@ -49,10 +49,20 @@ interface IProps {
size?: number;
onClick?: () => void;
hideTooltip?: boolean;
tooltipAlignment?: Alignment;
bordered?: boolean;
}
const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
const E2EIcon: React.FC<IProps> = ({
isUser,
status,
className,
size,
onClick,
hideTooltip,
tooltipAlignment,
bordered,
}) => {
const [hover, setHover] = useState(false);
const classes = classNames({
@ -80,7 +90,7 @@ const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, h
let tip;
if (hover && !hideTooltip) {
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />;
}
if (onClick) {

View file

@ -24,7 +24,6 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call";
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
import type { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserTab";
@ -32,7 +31,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon';
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { ButtonEvent } from "../elements/AccessibleButton";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
@ -57,14 +56,17 @@ import SdkConfig from "../../../SdkConfig";
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import { useWidgets } from "../right_panel/RoomSummaryCard";
import { WidgetType } from "../../../widgets/WidgetType";
import { useCall } from "../../../hooks/useCall";
import { useCall, useLayout } from "../../../hooks/useCall";
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
import { ElementCall } from "../../../models/Call";
import { Call, ElementCall, Layout } from "../../../models/Call";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { CallDurationFromEvent } from "../voip/CallDuration";
import { Alignment } from "../elements/Tooltip";
class DisabledWithReason {
constructor(public readonly reason: string) { }
@ -107,6 +109,7 @@ const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavi
onClick={onClick}
title={_t("Voice call")}
tooltip={tooltip ?? _t("Voice call")}
alignment={Alignment.Bottom}
disabled={disabled || busy}
/>;
};
@ -207,6 +210,7 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
onClick={onClick}
title={_t("Video call")}
tooltip={tooltip ?? _t("Video call")}
alignment={Alignment.Bottom}
disabled={disabled || busy}
/>
{ menu }
@ -318,6 +322,72 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
}
};
interface CallLayoutSelectorProps {
call: ElementCall;
}
const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
const layout = useLayout(call);
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
const onClick = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
openMenu();
}, [openMenu]);
const onFreedomClick = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
call.setLayout(Layout.Tile);
}, [closeMenu, call]);
const onSpotlightClick = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
call.setLayout(Layout.Spotlight);
}, [closeMenu, call]);
let menu: JSX.Element | null = null;
if (menuOpen) {
const buttonRect = buttonRef.current!.getBoundingClientRect();
menu = <IconizedContextMenu
className="mx_RoomHeader_layoutMenu"
{...aboveLeftOf(buttonRect)}
onFinished={closeMenu}
>
<IconizedContextMenuOptionList>
<IconizedContextMenuRadio
iconClassName="mx_RoomHeader_freedomIcon"
label={_t("Freedom")}
active={layout === Layout.Tile}
onClick={onFreedomClick}
/>
<IconizedContextMenuRadio
iconClassName="mx_RoomHeader_spotlightIcon"
label={_t("Spotlight")}
active={layout === Layout.Spotlight}
onClick={onSpotlightClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
return <>
<AccessibleTooltipButton
inputRef={buttonRef}
className={classNames("mx_RoomHeader_button", {
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
})}
onClick={onClick}
title={_t("Layout type")}
alignment={Alignment.Bottom}
key="layout"
/>
{ menu }
</>;
};
export interface ISearchInfo {
searchTerm: string;
searchScope: SearchScope;
@ -338,6 +408,8 @@ export interface IProps {
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
showButtons?: boolean;
enableRoomOptionsMenu?: boolean;
viewingCall: boolean;
activeCall: Call | null;
}
interface IState {
@ -356,6 +428,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
private readonly client = this.props.room.client;
constructor(props: IProps, context: IState) {
super(props, context);
@ -367,14 +440,12 @@ export default class RoomHeader extends React.Component<IProps, IState> {
}
public componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on(RoomStateEvent.Events, this.onRoomStateEvents);
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
}
public componentWillUnmount() {
const cli = MatrixClientPeg.get();
cli?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
@ -401,7 +472,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
this.forceUpdate();
}, 500, { leading: true, trailing: true });
private onContextMenuOpenClick = (ev: React.MouseEvent) => {
private onContextMenuOpenClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
@ -412,56 +483,98 @@ export default class RoomHeader extends React.Component<IProps, IState> {
this.setState({ contextMenuPosition: undefined });
};
private renderButtons(): JSX.Element[] {
const buttons: JSX.Element[] = [];
private onHideCallClick = (ev: ButtonEvent) => {
ev.preventDefault();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.props.room.roomId,
view_call: false,
metricsTrigger: undefined,
});
};
if (this.props.inRoom && !this.context.tombstone) {
buttons.push(<CallButtons key="calls" room={this.props.room} />);
private renderButtons(isVideoRoom: boolean): React.ReactNode {
const startButtons: JSX.Element[] = [];
if (!this.props.viewingCall && this.props.inRoom && !this.context.tombstone) {
startButtons.push(<CallButtons key="calls" room={this.props.room} />);
}
if (this.props.onForgetClick) {
const forgetButton = <AccessibleTooltipButton
if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) {
startButtons.push(<CallLayoutSelector call={this.props.activeCall} />);
}
if (!this.props.viewingCall && this.props.onForgetClick) {
startButtons.push(<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
onClick={this.props.onForgetClick}
title={_t("Forget room")}
alignment={Alignment.Bottom}
key="forget"
/>;
buttons.push(forgetButton);
/>);
}
if (this.props.onAppsClick) {
const appsButton = <AccessibleTooltipButton
if (!this.props.viewingCall && this.props.onAppsClick) {
startButtons.push(<AccessibleTooltipButton
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
})}
onClick={this.props.onAppsClick}
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
alignment={Alignment.Bottom}
key="apps"
/>;
buttons.push(appsButton);
/>);
}
if (this.props.onSearchClick && this.props.inRoom) {
const searchButton = <AccessibleTooltipButton
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
startButtons.push(<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
onClick={this.props.onSearchClick}
title={_t("Search")}
alignment={Alignment.Bottom}
key="search"
/>;
buttons.push(searchButton);
/>);
}
if (this.props.onInviteClick && this.props.inRoom) {
const inviteButton = <AccessibleTooltipButton
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
startButtons.push(<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
onClick={this.props.onInviteClick}
title={_t("Invite")}
alignment={Alignment.Bottom}
key="invite"
/>;
buttons.push(inviteButton);
/>);
}
return buttons;
const endButtons: JSX.Element[] = [];
if (this.props.viewingCall && !isVideoRoom) {
if (this.props.activeCall === null) {
endButtons.push(<AccessibleButton
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
onClick={this.onHideCallClick}
title={_t("Close call")}
key="close"
/>);
} else {
endButtons.push(<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
onClick={this.onHideCallClick}
title={_t("View chat timeline")}
alignment={Alignment.Bottom}
key="minimise"
/>);
}
}
return <>
{ startButtons }
<RoomHeaderButtons
room={this.props.room}
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
/>
{ endButtons }
</>;
}
private renderName(oobName: string) {
@ -480,7 +593,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
let settingsHint = false;
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
if (members) {
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
if (members.length === 1 && members[0].userId === this.client.credentials.userId) {
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
if (!nameEvent || !nameEvent.getContent().name) {
settingsHint = true;
@ -505,6 +618,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
onClick={this.onContextMenuOpenClick}
isExpanded={!!this.state.contextMenuPosition}
title={_t("Room options")}
alignment={Alignment.Bottom}
>
{ roomName }
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
@ -519,6 +633,57 @@ export default class RoomHeader extends React.Component<IProps, IState> {
}
public render() {
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
let roomAvatar: JSX.Element | null = null;
if (this.props.room) {
roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={24}
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>;
}
const icon = this.props.viewingCall
? <div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
: this.props.e2eStatus
? <E2EIcon
className="mx_RoomHeader_icon"
status={this.props.e2eStatus}
tooltipAlignment={Alignment.Bottom}
/>
// If we're expecting an E2EE status to come in, but it hasn't
// yet been loaded, insert a blank div to reserve space
: this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled()
? <div className="mx_RoomHeader_icon" />
: null;
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
if (this.props.viewingCall && !isVideoRoom) {
return (
<header className="mx_RoomHeader light-panel">
<div
className="mx_RoomHeader_wrapper"
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
>
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
{ icon }
<div className="mx_RoomHeader_name mx_RoomHeader_name--textonly mx_RoomHeader_name--small">
{ _t("Video call") }
</div>
{ this.props.activeCall instanceof ElementCall && (
<CallDurationFromEvent mxEvent={this.props.activeCall.groupCall} />
) }
{ /* Empty topic element to fill out space */ }
<div className="mx_RoomHeader_topic" />
{ buttons }
</div>
</header>
);
}
let searchStatus: JSX.Element | null = null;
// don't display the search count until the search completes and
@ -543,29 +708,6 @@ export default class RoomHeader extends React.Component<IProps, IState> {
className="mx_RoomHeader_topic"
/>;
let roomAvatar: JSX.Element | null = null;
if (this.props.room) {
roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={24}
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>;
}
let buttons: JSX.Element | null = null;
if (this.props.showButtons) {
buttons = <React.Fragment>
<div className="mx_RoomHeader_buttons">
{ this.renderButtons() }
</div>
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
</React.Fragment>;
}
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
const viewLabs = () => defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
@ -581,7 +723,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
>
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ icon }
{ name }
{ searchStatus }
{ topicElement }

View file

@ -89,7 +89,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
notificationsMenuPosition: null,
generalMenuPosition: null,
call: CallStore.instance.get(this.props.room.roomId),
call: CallStore.instance.getCall(this.props.room.roomId),
// generatePreview() will return nothing if the user has previews disabled
messagePreview: "",
};
@ -159,7 +159,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
// Recalculate the call for this room, since it could've changed between
// construction and mounting
this.setState({ call: CallStore.instance.get(this.props.room.roomId) });
this.setState({ call: CallStore.instance.getCall(this.props.room.roomId) });
}
public componentWillUnmount() {

View file

@ -32,7 +32,7 @@ const LegacyCallViewHeaderControls: React.FC<LegacyCallControlsProps> = ({ onExp
{ onMaximize && <AccessibleTooltipButton
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen"
onClick={onMaximize}
title={_t("Fill Screen")}
title={_t("Fill screen")}
/> }
{ onPin && <AccessibleTooltipButton
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin"

View file

@ -201,7 +201,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
};
return (
<div
<aside
className={this.props.className}
style={style}
ref={this.callViewWrapper}
@ -211,7 +211,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
onStartMoving: this.onStartMoving,
onResize: this.onResize,
}) }
</div>
</aside>
);
}
}

View file

@ -24,7 +24,6 @@ import LegacyCallView from "./LegacyCallView";
import { RoomViewStore } from '../../../stores/RoomViewStore';
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
import PersistentApp from "../elements/PersistentApp";
import SettingsStore from "../../../settings/SettingsStore";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PictureInPictureDragger from './PictureInPictureDragger';
import dis from '../../../dispatcher/dispatcher';
@ -35,6 +34,7 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/Activ
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import { CallStore } from "../../../stores/CallStore";
const SHOW_CALL_IN_STATES = [
CallState.Connected,
@ -116,7 +116,6 @@ function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall
*/
export default class PipView extends React.Component<IProps, IState> {
private settingsWatcherRef: string;
private movePersistedElement = createRef<() => void>();
constructor(props: IProps) {
@ -157,7 +156,6 @@ export default class PipView extends React.Component<IProps, IState> {
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SettingsStore.unwatchSetting(this.settingsWatcherRef);
const room = MatrixClientPeg.get().getRoom(this.state.viewedRoomId);
if (room) {
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
@ -278,6 +276,14 @@ export default class PipView extends React.Component<IProps, IState> {
});
};
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,
@ -323,18 +329,19 @@ export default class PipView extends React.Component<IProps, IState> {
mx_LegacyCallView_large: !pipMode,
});
const roomId = this.state.persistentRoomId;
const roomForWidget = MatrixClientPeg.get().getRoom(roomId);
const roomForWidget = MatrixClientPeg.get().getRoom(roomId)!;
const viewingCallRoom = this.state.viewedRoomId === roomId;
const isCall = CallStore.instance.getActiveCall(roomId) !== null;
pipContent = ({ onStartMoving, _onResize }) =>
pipContent = ({ onStartMoving }) =>
<div className={pipViewClasses}>
<LegacyCallViewHeader
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
pipMode={pipMode}
callRooms={[roomForWidget]}
onExpand={!viewingCallRoom && this.onExpand}
onPin={viewingCallRoom && this.onPin}
onMaximize={viewingCallRoom && this.onMaximize}
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}