Add ability to switch between voice & video in calls (#7155)
This commit is contained in:
parent
756b924966
commit
b5d11336f7
5 changed files with 42 additions and 80 deletions
|
@ -21,10 +21,13 @@ limitations under the License.
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.mx_CallViewHeader_pip {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_callType {
|
.mx_CallViewHeader_text {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -93,18 +96,7 @@ limitations under the License.
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_callTypeSmall {
|
.mx_CallViewHeader_icon {
|
||||||
font-size: 12px;
|
|
||||||
color: $secondary-content;
|
|
||||||
line-height: initial;
|
|
||||||
height: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallViewHeader_callTypeIcon {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
@ -122,13 +114,6 @@ limitations under the License.
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallViewHeader_callTypeIcon_voice::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewHeader_callTypeIcon_video::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, CSSProperties } from 'react';
|
import React, { createRef, CSSProperties } from 'react';
|
||||||
import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
|
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||||
|
@ -339,35 +339,38 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderCallControls(): JSX.Element {
|
private renderCallControls(): JSX.Element {
|
||||||
// We don't support call upgrades (yet) so hide the video mute button in voice calls
|
const { call, pipMode } = this.props;
|
||||||
const vidMuteButtonShown = this.props.call.type === CallType.Video;
|
const { primaryFeed, callState, micMuted, vidMuted, screensharing, sidebarShown } = this.state;
|
||||||
|
|
||||||
|
// If SDPStreamMetadata isn't supported don't show video mute button in voice calls
|
||||||
|
const vidMuteButtonShown = call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack;
|
||||||
// Screensharing is possible, if we can send a second stream and
|
// Screensharing is possible, if we can send a second stream and
|
||||||
// identify it using SDPStreamMetadata or if we can replace the already
|
// identify it using SDPStreamMetadata or if we can replace the already
|
||||||
// existing usermedia track by a screensharing track. We also need to be
|
// existing usermedia track by a screensharing track. We also need to be
|
||||||
// connected to know the state of the other side
|
// connected to know the state of the other side
|
||||||
const screensharingButtonShown = (
|
const screensharingButtonShown = (
|
||||||
(this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) &&
|
(call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack) &&
|
||||||
this.props.call.state === CallState.Connected
|
call.state === CallState.Connected
|
||||||
);
|
);
|
||||||
// To show the sidebar we need secondary feeds, if we don't have them,
|
// To show the sidebar we need secondary feeds, if we don't have them,
|
||||||
// we can hide this button. If we are in PiP, sidebar is also hidden, so
|
// we can hide this button. If we are in PiP, sidebar is also hidden, so
|
||||||
// we can hide the button too
|
// we can hide the button too
|
||||||
const sidebarButtonShown = (
|
const sidebarButtonShown = (
|
||||||
this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
|
primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
|
||||||
this.props.call.isScreensharing()
|
call.isScreensharing()
|
||||||
);
|
);
|
||||||
// The dial pad & 'more' button actions are only relevant in a connected call
|
// The dial pad & 'more' button actions are only relevant in a connected call
|
||||||
const contextMenuButtonShown = this.state.callState === CallState.Connected;
|
const contextMenuButtonShown = callState === CallState.Connected;
|
||||||
const dialpadButtonShown = (
|
const dialpadButtonShown = (
|
||||||
this.state.callState === CallState.Connected &&
|
callState === CallState.Connected &&
|
||||||
this.props.call.opponentSupportsDTMF()
|
call.opponentSupportsDTMF()
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CallViewButtons
|
<CallViewButtons
|
||||||
ref={this.buttonsRef}
|
ref={this.buttonsRef}
|
||||||
call={this.props.call}
|
call={call}
|
||||||
pipMode={this.props.pipMode}
|
pipMode={pipMode}
|
||||||
handlers={{
|
handlers={{
|
||||||
onToggleSidebarClick: this.onToggleSidebar,
|
onToggleSidebarClick: this.onToggleSidebar,
|
||||||
onScreenshareClick: this.onScreenshareClick,
|
onScreenshareClick: this.onScreenshareClick,
|
||||||
|
@ -376,10 +379,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
onVidMuteClick: this.onVidMuteClick,
|
onVidMuteClick: this.onVidMuteClick,
|
||||||
}}
|
}}
|
||||||
buttonsState={{
|
buttonsState={{
|
||||||
micMuted: this.state.micMuted,
|
micMuted: micMuted,
|
||||||
vidMuted: this.state.vidMuted,
|
vidMuted: vidMuted,
|
||||||
sidebarShown: this.state.sidebarShown,
|
sidebarShown: sidebarShown,
|
||||||
screensharing: this.state.screensharing,
|
screensharing: screensharing,
|
||||||
}}
|
}}
|
||||||
buttonsVisibility={{
|
buttonsVisibility={{
|
||||||
vidMute: vidMuteButtonShown,
|
vidMute: vidMuteButtonShown,
|
||||||
|
@ -406,7 +409,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
const someoneIsScreensharing = this.props.call.getFeeds().some((feed) => {
|
const someoneIsScreensharing = this.props.call.getFeeds().some((feed) => {
|
||||||
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
|
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
|
||||||
});
|
});
|
||||||
const isVideoCall = this.props.call.type === CallType.Video;
|
const call = this.props.call;
|
||||||
|
|
||||||
let contentView: React.ReactNode;
|
let contentView: React.ReactNode;
|
||||||
let holdTransferContent;
|
let holdTransferContent;
|
||||||
|
@ -461,7 +464,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
!isOnHold &&
|
!isOnHold &&
|
||||||
!transfereeCall &&
|
!transfereeCall &&
|
||||||
sidebarShown &&
|
sidebarShown &&
|
||||||
(isVideoCall || someoneIsScreensharing)
|
(call.hasLocalUserMediaVideoTrack || someoneIsScreensharing)
|
||||||
) {
|
) {
|
||||||
sidebar = (
|
sidebar = (
|
||||||
<CallViewSidebar
|
<CallViewSidebar
|
||||||
|
@ -474,7 +477,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// This is a bit messy. I can't see a reason to have two onHold/transfer screens
|
// This is a bit messy. I can't see a reason to have two onHold/transfer screens
|
||||||
if (isOnHold || transfereeCall) {
|
if (isOnHold || transfereeCall) {
|
||||||
if (isVideoCall) {
|
if (call.hasLocalUserMediaVideoTrack || call.hasRemoteUserMediaVideoTrack) {
|
||||||
const containerClasses = classNames({
|
const containerClasses = classNames({
|
||||||
mx_CallView_content: true,
|
mx_CallView_content: true,
|
||||||
mx_CallView_video: true,
|
mx_CallView_video: true,
|
||||||
|
@ -569,7 +572,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
let text = isScreensharing
|
let text = isScreensharing
|
||||||
? _t("You are presenting")
|
? _t("You are presenting")
|
||||||
: _t('%(sharerName)s is presenting', { sharerName });
|
: _t('%(sharerName)s is presenting', { sharerName });
|
||||||
if (!this.state.sidebarShown && isVideoCall) {
|
if (!this.state.sidebarShown) {
|
||||||
text += " • " + (this.props.call.isLocalVideoMuted()
|
text += " • " + (this.props.call.isLocalVideoMuted()
|
||||||
? _t("Your camera is turned off")
|
? _t("Your camera is turned off")
|
||||||
: _t("Your camera is still enabled"));
|
: _t("Your camera is still enabled"));
|
||||||
|
@ -613,7 +616,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
<CallViewHeader
|
<CallViewHeader
|
||||||
onPipMouseDown={this.props.onMouseDownOnHeader}
|
onPipMouseDown={this.props.onMouseDownOnHeader}
|
||||||
pipMode={this.props.pipMode}
|
pipMode={this.props.pipMode}
|
||||||
type={this.props.call.type}
|
|
||||||
callRooms={[callRoom, secCallRoom]}
|
callRooms={[callRoom, secCallRoom]}
|
||||||
/>
|
/>
|
||||||
{ contentView }
|
{ contentView }
|
||||||
|
|
|
@ -14,25 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CallType } from 'matrix-js-sdk/src/webrtc/call';
|
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { _t, _td } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import RoomAvatar from '../../avatars/RoomAvatar';
|
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||||
import dis from '../../../../dispatcher/dispatcher';
|
import dis from '../../../../dispatcher/dispatcher';
|
||||||
import { Action } from '../../../../dispatcher/actions';
|
import { Action } from '../../../../dispatcher/actions';
|
||||||
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||||
|
|
||||||
const callTypeTranslationByType: Record<CallType, string> = {
|
|
||||||
[CallType.Video]: _td("Video Call"),
|
|
||||||
[CallType.Voice]: _td("Voice Call"),
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CallViewHeaderProps {
|
interface CallViewHeaderProps {
|
||||||
pipMode: boolean;
|
pipMode: boolean;
|
||||||
type?: CallType;
|
|
||||||
callRooms?: Room[];
|
callRooms?: Room[];
|
||||||
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
@ -51,10 +43,10 @@ const onExpandClick = (roomId: string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode' | 'type'> & {
|
type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode'> & {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
};
|
};
|
||||||
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ pipMode = false, type, roomId }) => {
|
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ pipMode = false, roomId }) => {
|
||||||
return <div className="mx_CallViewHeader_controls">
|
return <div className="mx_CallViewHeader_controls">
|
||||||
{ !pipMode && <AccessibleTooltipButton
|
{ !pipMode && <AccessibleTooltipButton
|
||||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
||||||
|
@ -77,47 +69,33 @@ const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
|
||||||
</span>;
|
</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CallTypeIcon: React.FC<{ type: CallType }> = ({ type }) => {
|
|
||||||
const classes = classNames({
|
|
||||||
'mx_CallViewHeader_callTypeIcon': true,
|
|
||||||
'mx_CallViewHeader_callTypeIcon_video': type === CallType.Video,
|
|
||||||
'mx_CallViewHeader_callTypeIcon_voice': type === CallType.Voice,
|
|
||||||
});
|
|
||||||
return <div className={classes} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||||
type,
|
|
||||||
pipMode = false,
|
pipMode = false,
|
||||||
callRooms = [],
|
callRooms = [],
|
||||||
onPipMouseDown,
|
onPipMouseDown,
|
||||||
}) => {
|
}) => {
|
||||||
const [callRoom, onHoldCallRoom] = callRooms;
|
const [callRoom, onHoldCallRoom] = callRooms;
|
||||||
const callTypeText = type ? _t(callTypeTranslationByType[type]) : _t("Widget");
|
const callRoomName = callRoom.name;
|
||||||
const callRoomName = callRoom?.name;
|
const { roomId } = callRoom;
|
||||||
const roomId = callRoom?.roomId;
|
|
||||||
|
|
||||||
if (!pipMode) {
|
if (!pipMode) {
|
||||||
return <div className="mx_CallViewHeader">
|
return <div className="mx_CallViewHeader">
|
||||||
<CallTypeIcon type={type} />
|
<div className="mx_CallViewHeader_icon" />
|
||||||
<span className="mx_CallViewHeader_callType">{ callTypeText }</span>
|
<span className="mx_CallViewHeader_text">{ _t("Call") }</span>
|
||||||
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_CallViewHeader"
|
className="mx_CallViewHeader mx_CallViewHeader_pip"
|
||||||
onMouseDown={onPipMouseDown}
|
onMouseDown={onPipMouseDown}
|
||||||
>
|
>
|
||||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||||
<div className="mx_CallViewHeader_callInfo">
|
<div className="mx_CallViewHeader_callInfo">
|
||||||
<div className="mx_CallViewHeader_roomName" title={callRoomName}>{ callRoomName }</div>
|
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
||||||
<div className="mx_CallViewHeader_callTypeSmall">
|
|
||||||
{ callTypeText }
|
|
||||||
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} />
|
||||||
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -303,7 +303,6 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||||
pipContent = ({ onStartMoving, _onResize }) =>
|
pipContent = ({ onStartMoving, _onResize }) =>
|
||||||
<div className={pipViewClasses}>
|
<div className={pipViewClasses}>
|
||||||
<CallViewHeader
|
<CallViewHeader
|
||||||
type={undefined}
|
|
||||||
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
callRooms={[roomForWidget]}
|
callRooms={[roomForWidget]}
|
||||||
|
|
|
@ -1012,12 +1012,10 @@
|
||||||
"Show sidebar": "Show sidebar",
|
"Show sidebar": "Show sidebar",
|
||||||
"More": "More",
|
"More": "More",
|
||||||
"Hangup": "Hangup",
|
"Hangup": "Hangup",
|
||||||
"Video Call": "Video Call",
|
|
||||||
"Voice Call": "Voice Call",
|
|
||||||
"Fill Screen": "Fill Screen",
|
"Fill Screen": "Fill Screen",
|
||||||
"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",
|
||||||
"Widget": "Widget",
|
"Call": "Call",
|
||||||
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
||||||
"Verified!": "Verified!",
|
"Verified!": "Verified!",
|
||||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||||
|
|
Loading…
Reference in a new issue