Add a way to maximize/pin widget from the PiP view (#7672)
This commit is contained in:
parent
1a0af54ccb
commit
c79596cfe6
5 changed files with 112 additions and 46 deletions
|
@ -45,6 +45,8 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_CallViewHeader_controls {
|
.mx_CallViewHeader_controls {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_button {
|
.mx_CallViewHeader_button {
|
||||||
|
@ -63,17 +65,23 @@ limitations under the License.
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallViewHeader_button_fullscreen {
|
&.mx_CallViewHeader_button_fullscreen {
|
||||||
&::before {
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallViewHeader_button_expand {
|
&.mx_CallViewHeader_button_pin {
|
||||||
&::before {
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewHeader_button_expand {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -270,6 +270,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
return { primary, sidebar };
|
return { primary, sidebar };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMaximizeClick = (): void => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'video_fullscreen',
|
||||||
|
fullscreen: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onMicMuteClick = async (): Promise<void> => {
|
private onMicMuteClick = async (): Promise<void> => {
|
||||||
const newVal = !this.state.micMuted;
|
const newVal = !this.state.micMuted;
|
||||||
this.setState({ micMuted: await this.props.call.setMicrophoneMuted(newVal) });
|
this.setState({ micMuted: await this.props.call.setMicrophoneMuted(newVal) });
|
||||||
|
@ -614,6 +621,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
onPipMouseDown={onMouseDownOnHeader}
|
onPipMouseDown={onMouseDownOnHeader}
|
||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
callRooms={[callRoom, secCallRoom]}
|
callRooms={[callRoom, secCallRoom]}
|
||||||
|
onMaximize={this.onMaximizeClick}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CallView_content_wrapper" ref={this.contentWrapperRef}>
|
<div className="mx_CallView_content_wrapper" ref={this.contentWrapperRef}>
|
||||||
{ this.renderToast() }
|
{ this.renderToast() }
|
||||||
|
|
|
@ -19,50 +19,39 @@ import React from 'react';
|
||||||
|
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import RoomAvatar from '../../avatars/RoomAvatar';
|
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||||
import dis from '../../../../dispatcher/dispatcher';
|
|
||||||
import { Action } from '../../../../dispatcher/actions';
|
|
||||||
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||||
import { ViewRoomPayload } from "../../../../dispatcher/payloads/ViewRoomPayload";
|
|
||||||
|
|
||||||
interface CallViewHeaderProps {
|
interface CallControlsProps {
|
||||||
pipMode: boolean;
|
onExpand?: () => void;
|
||||||
callRooms?: Room[];
|
onPin?: () => void;
|
||||||
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
onMaximize?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFullscreenClick = () => {
|
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ onExpand, onPin, onMaximize }) => {
|
||||||
dis.dispatch({
|
|
||||||
action: 'video_fullscreen',
|
|
||||||
fullscreen: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExpandClick = (roomId: string) => {
|
|
||||||
dis.dispatch<ViewRoomPayload>({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: roomId,
|
|
||||||
metricsTrigger: "WebFloatingCallWindow",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode'> & {
|
|
||||||
roomId: string;
|
|
||||||
};
|
|
||||||
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ pipMode = false, roomId }) => {
|
|
||||||
return <div className="mx_CallViewHeader_controls">
|
return <div className="mx_CallViewHeader_controls">
|
||||||
{ !pipMode && <AccessibleTooltipButton
|
{ onMaximize && <AccessibleTooltipButton
|
||||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
||||||
onClick={onFullscreenClick}
|
onClick={onMaximize}
|
||||||
title={_t("Fill Screen")}
|
title={_t("Fill Screen")}
|
||||||
/> }
|
/> }
|
||||||
{ pipMode && <AccessibleTooltipButton
|
{ onPin && <AccessibleTooltipButton
|
||||||
|
className="mx_CallViewHeader_button mx_CallViewHeader_button_pin"
|
||||||
|
onClick={onPin}
|
||||||
|
title={_t("Pin")}
|
||||||
|
/> }
|
||||||
|
{ onExpand && <AccessibleTooltipButton
|
||||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_expand"
|
className="mx_CallViewHeader_button mx_CallViewHeader_button_expand"
|
||||||
onClick={() => onExpandClick(roomId)}
|
onClick={onExpand}
|
||||||
title={_t("Return to call")}
|
title={_t("Return to call")}
|
||||||
/> }
|
/> }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
|
|
||||||
|
interface ISecondaryCallInfoProps {
|
||||||
|
callRoom: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SecondaryCallInfo: React.FC<ISecondaryCallInfoProps> = ({ callRoom }) => {
|
||||||
return <span className="mx_CallViewHeader_secondaryCallInfo">
|
return <span className="mx_CallViewHeader_secondaryCallInfo">
|
||||||
<RoomAvatar room={callRoom} height={16} width={16} />
|
<RoomAvatar room={callRoom} height={16} width={16} />
|
||||||
<span className="mx_CallView_secondaryCall_roomName">
|
<span className="mx_CallView_secondaryCall_roomName">
|
||||||
|
@ -71,19 +60,31 @@ const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
|
||||||
</span>;
|
</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CallViewHeaderProps {
|
||||||
|
pipMode: boolean;
|
||||||
|
callRooms?: Room[];
|
||||||
|
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
onExpand?: () => void;
|
||||||
|
onPin?: () => void;
|
||||||
|
onMaximize?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||||
pipMode = false,
|
pipMode = false,
|
||||||
callRooms = [],
|
callRooms = [],
|
||||||
onPipMouseDown,
|
onPipMouseDown,
|
||||||
|
onExpand,
|
||||||
|
onPin,
|
||||||
|
onMaximize,
|
||||||
}) => {
|
}) => {
|
||||||
const [callRoom, onHoldCallRoom] = callRooms;
|
const [callRoom, onHoldCallRoom] = callRooms;
|
||||||
const { roomId, name: callRoomName } = callRoom;
|
const callRoomName = callRoom.name;
|
||||||
|
|
||||||
if (!pipMode) {
|
if (!pipMode) {
|
||||||
return <div className="mx_CallViewHeader">
|
return <div className="mx_CallViewHeader">
|
||||||
<div className="mx_CallViewHeader_icon" />
|
<div className="mx_CallViewHeader_icon" />
|
||||||
<span className="mx_CallViewHeader_text">{ _t("Call") }</span>
|
<span className="mx_CallViewHeader_text">{ _t("Call") }</span>
|
||||||
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} />
|
<CallViewHeaderControls onMaximize={onMaximize} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -96,7 +97,7 @@ const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||||
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
||||||
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||||
</div>
|
</div>
|
||||||
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} />
|
<CallViewHeaderControls onExpand={onExpand} onPin={onPin} onMaximize={onMaximize} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'
|
||||||
import { EventSubscription } from 'fbemitter';
|
import { EventSubscription } from 'fbemitter';
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import CallView from "./CallView";
|
import CallView from "./CallView";
|
||||||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||||
|
@ -29,9 +30,10 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
||||||
import CallViewHeader from './CallView/CallViewHeader';
|
import CallViewHeader from './CallView/CallViewHeader';
|
||||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
||||||
|
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
|
@ -64,6 +66,16 @@ interface IState {
|
||||||
moving: boolean;
|
moving: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room, IApp] => {
|
||||||
|
if (!widgetId) return;
|
||||||
|
if (!roomId) return;
|
||||||
|
|
||||||
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
const app = WidgetStore.instance.getApps(roomId).find((app) => app.id === widgetId);
|
||||||
|
|
||||||
|
return [room, app];
|
||||||
|
};
|
||||||
|
|
||||||
// Splits a list of calls into one 'primary' one and a list
|
// Splits a list of calls into one 'primary' one and a list
|
||||||
// (which should be a single element) of other calls.
|
// (which should be a single element) of other calls.
|
||||||
// The primary will be the one not on hold, or an arbitrary one
|
// The primary will be the one not on hold, or an arbitrary one
|
||||||
|
@ -232,6 +244,38 @@ export default 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);
|
||||||
|
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
|
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
|
||||||
public updateShowWidgetInPip(
|
public updateShowWidgetInPip(
|
||||||
persistentWidgetId = this.state.persistentWidgetId,
|
persistentWidgetId = this.state.persistentWidgetId,
|
||||||
|
@ -276,7 +320,9 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||||
mx_CallView_pip: pipMode,
|
mx_CallView_pip: pipMode,
|
||||||
mx_CallView_large: !pipMode,
|
mx_CallView_large: !pipMode,
|
||||||
});
|
});
|
||||||
const roomForWidget = MatrixClientPeg.get().getRoom(this.state.persistentRoomId);
|
const roomId = this.state.persistentRoomId;
|
||||||
|
const roomForWidget = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
const viewingCallRoom = this.state.viewedRoomId === roomId;
|
||||||
|
|
||||||
pipContent = ({ onStartMoving, _onResize }) =>
|
pipContent = ({ onStartMoving, _onResize }) =>
|
||||||
<div className={pipViewClasses}>
|
<div className={pipViewClasses}>
|
||||||
|
@ -284,10 +330,13 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||||
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
callRooms={[roomForWidget]}
|
callRooms={[roomForWidget]}
|
||||||
|
onExpand={!viewingCallRoom && this.onExpand}
|
||||||
|
onPin={viewingCallRoom && this.onPin}
|
||||||
|
onMaximize={viewingCallRoom && this.onMaximize}
|
||||||
/>
|
/>
|
||||||
<PersistentApp
|
<PersistentApp
|
||||||
persistentWidgetId={this.state.persistentWidgetId}
|
persistentWidgetId={this.state.persistentWidgetId}
|
||||||
persistentRoomId={this.state.persistentRoomId}
|
persistentRoomId={roomId}
|
||||||
pointerEvents={this.state.moving ? 'none' : undefined}
|
pointerEvents={this.state.moving ? 'none' : undefined}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -1045,6 +1045,7 @@
|
||||||
"More": "More",
|
"More": "More",
|
||||||
"Hangup": "Hangup",
|
"Hangup": "Hangup",
|
||||||
"Fill Screen": "Fill Screen",
|
"Fill Screen": "Fill Screen",
|
||||||
|
"Pin": "Pin",
|
||||||
"Return to call": "Return to call",
|
"Return to call": "Return to call",
|
||||||
"%(name)s on hold": "%(name)s on hold",
|
"%(name)s on hold": "%(name)s on hold",
|
||||||
"Call": "Call",
|
"Call": "Call",
|
||||||
|
@ -1128,7 +1129,6 @@
|
||||||
"Anchor": "Anchor",
|
"Anchor": "Anchor",
|
||||||
"Headphones": "Headphones",
|
"Headphones": "Headphones",
|
||||||
"Folder": "Folder",
|
"Folder": "Folder",
|
||||||
"Pin": "Pin",
|
|
||||||
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
|
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
|
||||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue