mirror of https://github.com/vector-im/riot-web
Add UI for hold functionality
parent
1db130b8f0
commit
2a02e57a95
|
@ -53,6 +53,7 @@
|
||||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||||
@import "./views/avatars/_PulsedAvatar.scss";
|
@import "./views/avatars/_PulsedAvatar.scss";
|
||||||
@import "./views/avatars/_WidgetAvatar.scss";
|
@import "./views/avatars/_WidgetAvatar.scss";
|
||||||
|
@import "./views/context_menus/_CallContextMenu.scss";
|
||||||
@import "./views/context_menus/_IconizedContextMenu.scss";
|
@import "./views/context_menus/_IconizedContextMenu.scss";
|
||||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 New Vector Ltd
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_CallContextMenu_item {
|
||||||
|
width: 205px;
|
||||||
|
height: 40px;
|
||||||
|
padding-left: 16px;
|
||||||
|
line-height: 40px;
|
||||||
|
vertical-align: center;
|
||||||
|
}
|
|
@ -43,17 +43,99 @@ limitations under the License.
|
||||||
.mx_CallView_voice {
|
.mx_CallView_voice {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_voice_hold {
|
||||||
|
// This masks the avatar image so when it's blurred, the edge is still crisp
|
||||||
|
.mx_CallView_voice_avatarContainer {
|
||||||
|
border-radius: 2000px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-image: url('$(res)/img/voip/paused.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
filter: blur(20px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_voice_holdText {
|
||||||
|
height: 16px;
|
||||||
|
color: $accent-fg-color;
|
||||||
|
.mx_AccessibleButton_hasKind {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallView_video {
|
.mx_CallView_video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_video_hold {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// we keep these around in the DOM: it saved wiring them up again when the call
|
||||||
|
// is resumed and keeps the container the right size
|
||||||
|
.mx_VideoFeed {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_video_holdBackground {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_video_holdContent {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-weight: bold;
|
||||||
|
color: $accent-fg-color;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
content: '';
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-image: url('$(res)/img/voip/paused.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
.mx_AccessibleButton_hasKind {
|
||||||
|
display: block;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallView_header {
|
.mx_CallView_header {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -173,6 +255,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Makes the alignment correct
|
||||||
|
.mx_CallView_callControls_nothing {
|
||||||
|
margin-right: auto;
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallView_callControls_button_micOn {
|
.mx_CallView_callControls_button_micOn {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url('$(res)/img/voip/mic-on.svg');
|
background-image: url('$(res)/img/voip/mic-on.svg');
|
||||||
|
@ -203,6 +291,18 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_callControls_button_more {
|
||||||
|
margin-left: auto;
|
||||||
|
&::before {
|
||||||
|
background-image: url('$(res)/img/voip/more.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_callControls_button_more_hidden {
|
||||||
|
margin-left: auto;
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallView_callControls_button_invisible {
|
.mx_CallView_callControls_button_invisible {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -398,7 +398,7 @@ export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => {
|
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||||
|
|
||||||
const buttonRight = elementRect.right + window.pageXOffset;
|
const buttonRight = elementRect.right + window.pageXOffset;
|
||||||
|
@ -408,9 +408,9 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
||||||
menuOptions.right = window.innerWidth - buttonRight;
|
menuOptions.right = window.innerWidth - buttonRight;
|
||||||
// Align the menu vertically on whichever side of the button has more space available.
|
// Align the menu vertically on whichever side of the button has more space available.
|
||||||
if (buttonBottom < window.innerHeight / 2) {
|
if (buttonBottom < window.innerHeight / 2) {
|
||||||
menuOptions.top = buttonBottom;
|
menuOptions.top = buttonBottom + vPadding;
|
||||||
} else {
|
} else {
|
||||||
menuOptions.bottom = window.innerHeight - buttonTop;
|
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 New Vector Ltd
|
||||||
|
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { ContextMenu, IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
||||||
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
|
||||||
|
interface IProps extends IContextMenuProps {
|
||||||
|
call: MatrixCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CallContextMenu extends React.Component<IProps> {
|
||||||
|
static propTypes = {
|
||||||
|
// js-sdk User object. Not required because it might not exist.
|
||||||
|
user: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
onHoldUnholdClick = () => {
|
||||||
|
this.props.call.setRemoteOnHold(!this.props.call.isRemoteOnHold());
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold");
|
||||||
|
|
||||||
|
return <ContextMenu {...this.props}>
|
||||||
|
<MenuItem className="mx_CallContextMenu_item" onClick={this.onHoldUnholdClick}>
|
||||||
|
{holdUnholdCaption}
|
||||||
|
</MenuItem>
|
||||||
|
</ContextMenu>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef, CSSProperties } from 'react';
|
||||||
import Room from 'matrix-js-sdk/src/models/room';
|
import Room from 'matrix-js-sdk/src/models/room';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import CallHandler from '../../../CallHandler';
|
import CallHandler from '../../../CallHandler';
|
||||||
|
@ -28,6 +28,9 @@ import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
||||||
|
import {aboveLeftOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu';
|
||||||
|
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||||
|
import { avatarUrlForMember } from '../../../Avatar';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// js-sdk room object. If set, we will only show calls for the given
|
// js-sdk room object. If set, we will only show calls for the given
|
||||||
|
@ -51,10 +54,12 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
call: MatrixCall;
|
call: MatrixCall;
|
||||||
isLocalOnHold: boolean,
|
isLocalOnHold: boolean,
|
||||||
|
isRemoteOnHold: boolean,
|
||||||
micMuted: boolean,
|
micMuted: boolean,
|
||||||
vidMuted: boolean,
|
vidMuted: boolean,
|
||||||
callState: CallState,
|
callState: CallState,
|
||||||
controlsVisible: boolean,
|
controlsVisible: boolean,
|
||||||
|
showMoreMenu: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFullScreenElement() {
|
function getFullScreenElement() {
|
||||||
|
@ -89,11 +94,14 @@ const CONTROLS_HIDE_DELAY = 1000;
|
||||||
// Height of the header duplicated from CSS because we need to subtract it from our max
|
// Height of the header duplicated from CSS because we need to subtract it from our max
|
||||||
// height to get the max height of the video
|
// height to get the max height of the video
|
||||||
const HEADER_HEIGHT = 44;
|
const HEADER_HEIGHT = 44;
|
||||||
|
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
|
||||||
|
|
||||||
export default class CallView extends React.Component<IProps, IState> {
|
export default class CallView extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private contentRef = createRef<HTMLDivElement>();
|
private contentRef = createRef<HTMLDivElement>();
|
||||||
private controlsHideTimer: number = null;
|
private controlsHideTimer: number = null;
|
||||||
|
private contextMenuButton = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -101,10 +109,12 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
this.state = {
|
this.state = {
|
||||||
call,
|
call,
|
||||||
isLocalOnHold: call ? call.isLocalOnHold() : null,
|
isLocalOnHold: call ? call.isLocalOnHold() : null,
|
||||||
|
isRemoteOnHold: call ? call.isRemoteOnHold() : null,
|
||||||
micMuted: call ? call.isMicrophoneMuted() : null,
|
micMuted: call ? call.isMicrophoneMuted() : null,
|
||||||
vidMuted: call ? call.isLocalVideoMuted() : null,
|
vidMuted: call ? call.isLocalVideoMuted() : null,
|
||||||
callState: call ? call.state : null,
|
callState: call ? call.state : null,
|
||||||
controlsVisible: true,
|
controlsVisible: true,
|
||||||
|
showMoreMenu: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateCallListeners(null, call);
|
this.updateCallListeners(null, call);
|
||||||
|
@ -149,11 +159,16 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
call: newCall,
|
call: newCall,
|
||||||
isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
|
isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
|
||||||
|
isRemoteOnHold: newCall ? newCall.isRemoteOnHold() : null,
|
||||||
micMuted: newCall ? newCall.isMicrophoneMuted() : null,
|
micMuted: newCall ? newCall.isMicrophoneMuted() : null,
|
||||||
vidMuted: newCall ? newCall.isLocalVideoMuted() : null,
|
vidMuted: newCall ? newCall.isLocalVideoMuted() : null,
|
||||||
callState: newCall ? newCall.state : null,
|
callState: newCall ? newCall.state : null,
|
||||||
controlsVisible: newControlsVisible,
|
controlsVisible: newControlsVisible,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
callState: newCall ? newCall.state : null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!newCall && getFullScreenElement()) {
|
if (!newCall && getFullScreenElement()) {
|
||||||
exitFullscreen();
|
exitFullscreen();
|
||||||
|
@ -187,16 +202,30 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
|
private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
|
||||||
if (oldCall === newCall) return;
|
if (oldCall === newCall) return;
|
||||||
|
|
||||||
if (oldCall) oldCall.removeListener(CallEvent.HoldUnhold, this.onCallHoldUnhold);
|
if (oldCall) {
|
||||||
if (newCall) newCall.on(CallEvent.HoldUnhold, this.onCallHoldUnhold);
|
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||||
|
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||||
|
}
|
||||||
|
if (newCall) {
|
||||||
|
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||||
|
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCallHoldUnhold = () => {
|
private onCallLocalHoldUnhold = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLocalOnHold: this.state.call ? this.state.call.isLocalOnHold() : null,
|
isLocalOnHold: this.state.call ? this.state.call.isLocalOnHold() : null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onCallRemoteHoldUnhold = () => {
|
||||||
|
this.setState({
|
||||||
|
isRemoteOnHold: this.state.call ? this.state.call.isRemoteOnHold() : null,
|
||||||
|
// update both here because isLocalOnHold changes when we hold the call too
|
||||||
|
isLocalOnHold: this.state.call ? this.state.call.isLocalOnHold() : null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onFullscreenClick = () => {
|
private onFullscreenClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'video_fullscreen',
|
action: 'video_fullscreen',
|
||||||
|
@ -223,6 +252,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private showControls() {
|
private showControls() {
|
||||||
|
if (this.state.showMoreMenu) return;
|
||||||
|
|
||||||
if (!this.state.controlsVisible) {
|
if (!this.state.controlsVisible) {
|
||||||
this.setState({
|
this.setState({
|
||||||
controlsVisible: true,
|
controlsVisible: true,
|
||||||
|
@ -252,6 +283,25 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
this.setState({vidMuted: newVal});
|
this.setState({vidMuted: newVal});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMoreClick = () => {
|
||||||
|
if (this.controlsHideTimer) {
|
||||||
|
clearTimeout(this.controlsHideTimer);
|
||||||
|
this.controlsHideTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showMoreMenu: true,
|
||||||
|
controlsVisible: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeContextMenu = () => {
|
||||||
|
this.setState({
|
||||||
|
showMoreMenu: false,
|
||||||
|
});
|
||||||
|
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||||
// Note that this assumes we always have a callview on screen at any given time
|
// Note that this assumes we always have a callview on screen at any given time
|
||||||
// CallHandler would probably be a better place for this
|
// CallHandler would probably be a better place for this
|
||||||
|
@ -292,14 +342,32 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onCallResumeClick = () => {
|
||||||
|
this.state.call.setRemoteOnHold(false);
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
if (!this.state.call) return null;
|
if (!this.state.call) return null;
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const callRoom = client.getRoom(this.state.call.roomId);
|
const callRoom = client.getRoom(this.state.call.roomId);
|
||||||
|
|
||||||
|
let contextMenu;
|
||||||
|
|
||||||
let callControls;
|
let callControls;
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
|
if (this.state.showMoreMenu) {
|
||||||
|
contextMenu = <CallContextMenu
|
||||||
|
{...aboveLeftOf(
|
||||||
|
this.contextMenuButton.current.getBoundingClientRect(),
|
||||||
|
ChevronFace.None,
|
||||||
|
CONTEXT_MENU_VPADDING,
|
||||||
|
)}
|
||||||
|
onFinished={this.closeContextMenu}
|
||||||
|
call={this.state.call}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
const micClasses = classNames({
|
const micClasses = classNames({
|
||||||
mx_CallView_callControls_button: true,
|
mx_CallView_callControls_button: true,
|
||||||
mx_CallView_callControls_button_micOn: !this.state.micMuted,
|
mx_CallView_callControls_button_micOn: !this.state.micMuted,
|
||||||
|
@ -333,17 +401,29 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
mx_CallView_callControls_hidden: !this.state.controlsVisible,
|
mx_CallView_callControls_hidden: !this.state.controlsVisible,
|
||||||
});
|
});
|
||||||
|
|
||||||
const vidMuteButton = this.state.call.type === CallType.Video ? <div
|
const vidMuteButton = this.state.call.type === CallType.Video ? <AccessibleButton
|
||||||
className={vidClasses}
|
className={vidClasses}
|
||||||
onClick={this.onVidMuteClick}
|
onClick={this.onVidMuteClick}
|
||||||
/> : null;
|
/> : null;
|
||||||
|
|
||||||
|
// The 'more' button actions are only relevant in a connected call
|
||||||
|
// When not connected, we have to put something there to make the flexbox alignment correct
|
||||||
|
const contextMenuButton = this.state.callState === CallState.Connected ? <ContextMenuButton
|
||||||
|
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
||||||
|
onClick={this.onMoreClick}
|
||||||
|
inputRef={this.contextMenuButton}
|
||||||
|
isExpanded={this.state.showMoreMenu}
|
||||||
|
/> : <div className="mx_CallView_callControls_button mx_CallView_callControls_button_more_hidden" />;
|
||||||
|
|
||||||
|
// in the near future, the dial pad button will go on the left. For now, it's the nothing button
|
||||||
|
// because something needs to have margin-right: auto to make the alignment correct.
|
||||||
callControls = <div className={callControlsClasses}>
|
callControls = <div className={callControlsClasses}>
|
||||||
<div
|
<div className="mx_CallView_callControls_button mx_CallView_callControls_nothing" />
|
||||||
|
<AccessibleButton
|
||||||
className={micClasses}
|
className={micClasses}
|
||||||
onClick={this.onMicMuteClick}
|
onClick={this.onMicMuteClick}
|
||||||
/>
|
/>
|
||||||
<div
|
<AccessibleButton
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -355,6 +435,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
{vidMuteButton}
|
{vidMuteButton}
|
||||||
<div className={micCacheClasses} />
|
<div className={micCacheClasses} />
|
||||||
<div className={vidCacheClasses} />
|
<div className={vidCacheClasses} />
|
||||||
|
{contextMenuButton}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,24 +443,66 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
// for voice calls (fills the bg)
|
// for voice calls (fills the bg)
|
||||||
let contentView: React.ReactNode;
|
let contentView: React.ReactNode;
|
||||||
|
|
||||||
|
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
|
||||||
|
let onHoldText = null;
|
||||||
|
if (this.state.isRemoteOnHold) {
|
||||||
|
onHoldText = _t("You held the call <a>Resume</a>", {}, {
|
||||||
|
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
|
||||||
|
{sub}
|
||||||
|
</AccessibleButton>,
|
||||||
|
});
|
||||||
|
} else if (this.state.isLocalOnHold) {
|
||||||
|
onHoldText = _t("%(peerName)s held the call", {
|
||||||
|
peerName: this.state.call.getOpponentMember().name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.call.type === CallType.Video) {
|
if (this.state.call.type === CallType.Video) {
|
||||||
|
let onHoldContent = null;
|
||||||
|
let onHoldBackground = null;
|
||||||
|
const backgroundStyle: CSSProperties = {};
|
||||||
|
const containerClasses = classNames({
|
||||||
|
mx_CallView_video: true,
|
||||||
|
mx_CallView_video_hold: isOnHold,
|
||||||
|
});
|
||||||
|
if (isOnHold) {
|
||||||
|
onHoldContent = <div className="mx_CallView_video_holdContent">
|
||||||
|
{onHoldText}
|
||||||
|
</div>;
|
||||||
|
const backgroundAvatarUrl = avatarUrlForMember(
|
||||||
|
// is it worth getting the size of the div to pass here?
|
||||||
|
this.state.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||||
|
);
|
||||||
|
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||||
|
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||||
|
}
|
||||||
|
|
||||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||||
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight - HEADER_HEIGHT;
|
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight - HEADER_HEIGHT;
|
||||||
contentView = <div className="mx_CallView_video" ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||||
|
{onHoldBackground}
|
||||||
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
|
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
|
||||||
maxHeight={maxVideoHeight}
|
maxHeight={maxVideoHeight}
|
||||||
/>
|
/>
|
||||||
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
|
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
|
||||||
|
{onHoldContent}
|
||||||
{callControls}
|
{callControls}
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
const avatarSize = this.props.room ? 200 : 75;
|
const avatarSize = this.props.room ? 200 : 75;
|
||||||
contentView = <div className="mx_CallView_voice" onMouseMove={this.onMouseMove}>
|
const classes = classNames({
|
||||||
<RoomAvatar
|
mx_CallView_voice: true,
|
||||||
room={callRoom}
|
mx_CallView_voice_hold: isOnHold,
|
||||||
height={avatarSize}
|
});
|
||||||
width={avatarSize}
|
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
||||||
/>
|
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
|
||||||
|
<RoomAvatar
|
||||||
|
room={callRoom}
|
||||||
|
height={avatarSize}
|
||||||
|
width={avatarSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
|
||||||
{callControls}
|
{callControls}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -431,6 +554,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
return <div className={"mx_CallView " + myClassName}>
|
return <div className={"mx_CallView " + myClassName}>
|
||||||
{header}
|
{header}
|
||||||
{contentView}
|
{contentView}
|
||||||
|
{contextMenu}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -836,6 +836,8 @@
|
||||||
"When rooms are upgraded": "When rooms are upgraded",
|
"When rooms are upgraded": "When rooms are upgraded",
|
||||||
"My Ban List": "My Ban List",
|
"My Ban List": "My Ban List",
|
||||||
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
||||||
|
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||||
|
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||||
"Video Call": "Video Call",
|
"Video Call": "Video Call",
|
||||||
"Voice Call": "Voice Call",
|
"Voice Call": "Voice Call",
|
||||||
"Fill Screen": "Fill Screen",
|
"Fill Screen": "Fill Screen",
|
||||||
|
@ -2231,6 +2233,8 @@
|
||||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
|
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
|
||||||
"Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
|
"Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
|
||||||
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
|
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
|
||||||
|
"Resume": "Resume",
|
||||||
|
"Hold": "Hold",
|
||||||
"Reject invitation": "Reject invitation",
|
"Reject invitation": "Reject invitation",
|
||||||
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
|
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
|
||||||
"Unable to reject invite": "Unable to reject invite",
|
"Unable to reject invite": "Unable to reject invite",
|
||||||
|
|
Loading…
Reference in New Issue