Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Add ability to switch between voice & video in calls (#7155)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonBrandner committed Jan 24, 2022
1 parent 756b924 commit b5d1133
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 80 deletions.
27 changes: 6 additions & 21 deletions res/css/views/voip/_CallViewHeader.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ limitations under the License.
align-items: center;
justify-content: left;
flex-shrink: 0;
cursor: pointer;

&.mx_CallViewHeader_pip {
cursor: pointer;
}
}

.mx_CallViewHeader_callType {
.mx_CallViewHeader_text {
font-size: 1.2rem;
font-weight: bold;
vertical-align: middle;
Expand Down Expand Up @@ -93,18 +96,7 @@ limitations under the License.
margin-left: 4px;
}

.mx_CallViewHeader_callTypeSmall {
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 {
.mx_CallViewHeader_icon {
display: inline-block;
margin-right: 6px;
height: 16px;
Expand All @@ -122,13 +114,6 @@ limitations under the License.
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}

&.mx_CallViewHeader_callTypeIcon_voice::before {
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');
}
}
44 changes: 23 additions & 21 deletions src/components/views/voip/CallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.
*/

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 { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
Expand Down Expand Up @@ -339,35 +339,38 @@ export default class CallView extends React.Component<IProps, IState> {
};

private renderCallControls(): JSX.Element {
// We don't support call upgrades (yet) so hide the video mute button in voice calls
const vidMuteButtonShown = this.props.call.type === CallType.Video;
const { call, pipMode } = this.props;
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
// identify it using SDPStreamMetadata or if we can replace the already
// existing usermedia track by a screensharing track. We also need to be
// connected to know the state of the other side
const screensharingButtonShown = (
(this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) &&
this.props.call.state === CallState.Connected
(call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack) &&
call.state === CallState.Connected
);
// 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 the button too
const sidebarButtonShown = (
this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
this.props.call.isScreensharing()
primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
call.isScreensharing()
);
// 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 = (
this.state.callState === CallState.Connected &&
this.props.call.opponentSupportsDTMF()
callState === CallState.Connected &&
call.opponentSupportsDTMF()
);

return (
<CallViewButtons
ref={this.buttonsRef}
call={this.props.call}
pipMode={this.props.pipMode}
call={call}
pipMode={pipMode}
handlers={{
onToggleSidebarClick: this.onToggleSidebar,
onScreenshareClick: this.onScreenshareClick,
Expand All @@ -376,10 +379,10 @@ export default class CallView extends React.Component<IProps, IState> {
onVidMuteClick: this.onVidMuteClick,
}}
buttonsState={{
micMuted: this.state.micMuted,
vidMuted: this.state.vidMuted,
sidebarShown: this.state.sidebarShown,
screensharing: this.state.screensharing,
micMuted: micMuted,
vidMuted: vidMuted,
sidebarShown: sidebarShown,
screensharing: screensharing,
}}
buttonsVisibility={{
vidMute: vidMuteButtonShown,
Expand All @@ -406,7 +409,7 @@ export default class CallView extends React.Component<IProps, IState> {
const someoneIsScreensharing = this.props.call.getFeeds().some((feed) => {
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
});
const isVideoCall = this.props.call.type === CallType.Video;
const call = this.props.call;

let contentView: React.ReactNode;
let holdTransferContent;
Expand Down Expand Up @@ -461,7 +464,7 @@ export default class CallView extends React.Component<IProps, IState> {
!isOnHold &&
!transfereeCall &&
sidebarShown &&
(isVideoCall || someoneIsScreensharing)
(call.hasLocalUserMediaVideoTrack || someoneIsScreensharing)
) {
sidebar = (
<CallViewSidebar
Expand All @@ -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
if (isOnHold || transfereeCall) {
if (isVideoCall) {
if (call.hasLocalUserMediaVideoTrack || call.hasRemoteUserMediaVideoTrack) {
const containerClasses = classNames({
mx_CallView_content: true,
mx_CallView_video: true,
Expand Down Expand Up @@ -569,7 +572,7 @@ export default class CallView extends React.Component<IProps, IState> {
let text = isScreensharing
? _t("You are presenting")
: _t('%(sharerName)s is presenting', { sharerName });
if (!this.state.sidebarShown && isVideoCall) {
if (!this.state.sidebarShown) {
text += " • " + (this.props.call.isLocalVideoMuted()
? _t("Your camera is turned off")
: _t("Your camera is still enabled"));
Expand Down Expand Up @@ -613,7 +616,6 @@ export default class CallView extends React.Component<IProps, IState> {
<CallViewHeader
onPipMouseDown={this.props.onMouseDownOnHeader}
pipMode={this.props.pipMode}
type={this.props.call.type}
callRooms={[callRoom, secCallRoom]}
/>
{ contentView }
Expand Down
46 changes: 12 additions & 34 deletions src/components/views/voip/CallView/CallViewHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { CallType } from 'matrix-js-sdk/src/webrtc/call';
import { Room } from 'matrix-js-sdk/src/models/room';
import React from 'react';
import classNames from 'classnames';

import { _t, _td } from '../../../../languageHandler';
import { _t } from '../../../../languageHandler';
import RoomAvatar from '../../avatars/RoomAvatar';
import dis from '../../../../dispatcher/dispatcher';
import { Action } from '../../../../dispatcher/actions';
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';

const callTypeTranslationByType: Record<CallType, string> = {
[CallType.Video]: _td("Video Call"),
[CallType.Voice]: _td("Voice Call"),
};

interface CallViewHeaderProps {
pipMode: boolean;
type?: CallType;
callRooms?: Room[];
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
}
Expand All @@ -51,10 +43,10 @@ const onExpandClick = (roomId: string) => {
});
};

type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode' | 'type'> & {
type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode'> & {
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">
{ !pipMode && <AccessibleTooltipButton
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
Expand All @@ -77,47 +69,33 @@ const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
</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> = ({
type,
pipMode = false,
callRooms = [],
onPipMouseDown,
}) => {
const [callRoom, onHoldCallRoom] = callRooms;
const callTypeText = type ? _t(callTypeTranslationByType[type]) : _t("Widget");
const callRoomName = callRoom?.name;
const roomId = callRoom?.roomId;
const callRoomName = callRoom.name;
const { roomId } = callRoom;

if (!pipMode) {
return <div className="mx_CallViewHeader">
<CallTypeIcon type={type} />
<span className="mx_CallViewHeader_callType">{ callTypeText }</span>
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
<div className="mx_CallViewHeader_icon" />
<span className="mx_CallViewHeader_text">{ _t("Call") }</span>
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} />
</div>;
}
return (
<div
className="mx_CallViewHeader"
className="mx_CallViewHeader mx_CallViewHeader_pip"
onMouseDown={onPipMouseDown}
>
<RoomAvatar room={callRoom} height={32} width={32} />
<div className="mx_CallViewHeader_callInfo">
<div className="mx_CallViewHeader_roomName" title={callRoomName}>{ callRoomName }</div>
<div className="mx_CallViewHeader_callTypeSmall">
{ callTypeText }
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
</div>
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
</div>
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} />
</div>
);
};
Expand Down
1 change: 0 additions & 1 deletion src/components/views/voip/PipView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ export default class PipView extends React.Component<IProps, IState> {
pipContent = ({ onStartMoving, _onResize }) =>
<div className={pipViewClasses}>
<CallViewHeader
type={undefined}
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
pipMode={pipMode}
callRooms={[roomForWidget]}
Expand Down
4 changes: 1 addition & 3 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1012,12 +1012,10 @@
"Show sidebar": "Show sidebar",
"More": "More",
"Hangup": "Hangup",
"Video Call": "Video Call",
"Voice Call": "Voice Call",
"Fill Screen": "Fill Screen",
"Return to call": "Return to call",
"%(name)s on hold": "%(name)s on hold",
"Widget": "Widget",
"Call": "Call",
"The other party cancelled the verification.": "The other party cancelled the verification.",
"Verified!": "Verified!",
"You've successfully verified this user.": "You've successfully verified this user.",
Expand Down

0 comments on commit b5d1133

Please sign in to comment.