commit
ae2cfb494a
|
@ -20,14 +20,13 @@ limitations under the License.
|
||||||
background-color: $voipcall-plinth-color;
|
background-color: $voipcall-plinth-color;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
margin: 5px 5px 5px 18px;
|
|
||||||
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
|
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_large {
|
.mx_CallView_large {
|
||||||
// XXX: This should be 10 but somehow it's gaining an extra 4px from somewhere...
|
padding-bottom: 10px;
|
||||||
padding-bottom: 6px;
|
margin: 5px 5px 5px 18px;
|
||||||
|
|
||||||
.mx_CallView_voice {
|
.mx_CallView_voice {
|
||||||
height: 360px;
|
height: 360px;
|
||||||
|
@ -40,6 +39,22 @@ limitations under the License.
|
||||||
.mx_CallView_voice {
|
.mx_CallView_voice {
|
||||||
height: 180px;
|
height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_callControls {
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_callControls_button {
|
||||||
|
&::before {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_voice_holdText {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_voice {
|
.mx_CallView_voice {
|
||||||
|
@ -52,6 +67,17 @@ limitations under the License.
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_voice_avatarsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
div {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallView_voice_hold {
|
.mx_CallView_voice_hold {
|
||||||
// This masks the avatar image so when it's blurred, the edge is still crisp
|
// This masks the avatar image so when it's blurred, the edge is still crisp
|
||||||
.mx_CallView_voice_avatarContainer {
|
.mx_CallView_voice_avatarContainer {
|
||||||
|
@ -82,9 +108,33 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_voice_secondaryAvatarContainer {
|
||||||
|
border-radius: 2000px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
background-image: url('$(res)/img/voip/paused.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-size: 40px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.mx_CallView_pip &::after {
|
||||||
|
background-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallView_voice_holdText {
|
.mx_CallView_voice_holdText {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
color: $accent-fg-color;
|
color: $accent-fg-color;
|
||||||
.mx_AccessibleButton_hasKind {
|
.mx_AccessibleButton_hasKind {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
@ -167,10 +217,6 @@ limitations under the License.
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
|
|
||||||
.mx_BaseAvatar {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_header_callType {
|
.mx_CallView_header_callType {
|
||||||
|
@ -179,6 +225,14 @@ limitations under the License.
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_header_secondaryCallInfo {
|
||||||
|
&::before {
|
||||||
|
content: '·';
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallView_header_controls {
|
.mx_CallView_header_controls {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
@ -213,16 +267,31 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CallView_header_callInfo {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CallView_header_roomName {
|
.mx_CallView_header_roomName {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: initial;
|
line-height: initial;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_secondaryCall_roomName {
|
||||||
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_header_callTypeSmall {
|
.mx_CallView_header_callTypeSmall {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: $secondary-fg-color;
|
color: $secondary-fg-color;
|
||||||
line-height: initial;
|
line-height: initial;
|
||||||
|
height: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_header_phoneIcon {
|
.mx_CallView_header_phoneIcon {
|
||||||
|
|
|
@ -81,6 +81,7 @@ import Analytics from './Analytics';
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import {UIFeature} from "./settings/UIFeature";
|
import {UIFeature} from "./settings/UIFeature";
|
||||||
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
|
|
||||||
enum AudioID {
|
enum AudioID {
|
||||||
Ring = 'ringAudio',
|
Ring = 'ringAudio',
|
||||||
|
@ -115,7 +116,7 @@ function getRemoteAudioElement(): HTMLAudioElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallHandler {
|
export default class CallHandler {
|
||||||
private calls = new Map<string, MatrixCall>();
|
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||||
|
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
|
@ -175,6 +176,28 @@ export default class CallHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllActiveCalls() {
|
||||||
|
const activeCalls = [];
|
||||||
|
|
||||||
|
for (const call of this.calls.values()) {
|
||||||
|
if (call.state !== CallState.Ended && call.state !== CallState.Ringing) {
|
||||||
|
activeCalls.push(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return activeCalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllActiveCallsNotInRoom(notInThisRoomId) {
|
||||||
|
const callsNotInThatRoom = [];
|
||||||
|
|
||||||
|
for (const [roomId, call] of this.calls.entries()) {
|
||||||
|
if (roomId !== notInThisRoomId && call.state !== CallState.Ended) {
|
||||||
|
callsNotInThatRoom.push(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return callsNotInThatRoom;
|
||||||
|
}
|
||||||
|
|
||||||
play(audioId: AudioID) {
|
play(audioId: AudioID) {
|
||||||
// TODO: Attach an invisible element for this instead
|
// TODO: Attach an invisible element for this instead
|
||||||
// which listens?
|
// which listens?
|
||||||
|
@ -425,6 +448,8 @@ export default class CallHandler {
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
this.setCallAudioElement(call);
|
this.setCallAudioElement(call);
|
||||||
|
|
||||||
|
this.setActiveCallRoomId(roomId);
|
||||||
|
|
||||||
if (type === PlaceCallType.Voice) {
|
if (type === PlaceCallType.Voice) {
|
||||||
call.placeVoiceCall();
|
call.placeVoiceCall();
|
||||||
} else if (type === 'video') {
|
} else if (type === 'video') {
|
||||||
|
@ -453,14 +478,6 @@ export default class CallHandler {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'place_call':
|
case 'place_call':
|
||||||
{
|
{
|
||||||
if (this.getAnyActiveCall()) {
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
|
||||||
title: _t('Existing Call'),
|
|
||||||
description: _t('You are already in a call.'),
|
|
||||||
});
|
|
||||||
return; // don't allow >1 call to be placed.
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the runtime env doesn't do VoIP, whine.
|
// if the runtime env doesn't do VoIP, whine.
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
|
@ -470,6 +487,15 @@ export default class CallHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// don't allow > 2 calls to be placed.
|
||||||
|
if (this.getAllActiveCalls().length > 1) {
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||||
|
title: _t('Too Many Calls'),
|
||||||
|
description: _t("You've reached the maximum number of simultaneous calls."),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("Room %s does not exist.", payload.room_id);
|
console.error("Room %s does not exist.", payload.room_id);
|
||||||
|
@ -513,24 +539,21 @@ export default class CallHandler {
|
||||||
break;
|
break;
|
||||||
case 'incoming_call':
|
case 'incoming_call':
|
||||||
{
|
{
|
||||||
if (this.getAnyActiveCall()) {
|
|
||||||
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
|
||||||
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
|
||||||
// in future we could signal a "local busy" as a warning to the caller.
|
|
||||||
// see https://github.com/vector-im/vector-web/issues/1964
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the runtime env doesn't do VoIP, stop here.
|
// if the runtime env doesn't do VoIP, stop here.
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const call = payload.call as MatrixCall;
|
const call = payload.call as MatrixCall;
|
||||||
|
|
||||||
|
if (this.getCallForRoom(call.roomId)) {
|
||||||
|
// ignore multiple incoming calls to the same room
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||||
this.calls.set(call.roomId, call)
|
this.calls.set(call.roomId, call)
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
this.setCallAudioElement(call);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'hangup':
|
case 'hangup':
|
||||||
|
@ -543,14 +566,26 @@ export default class CallHandler {
|
||||||
} else {
|
} else {
|
||||||
this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false);
|
this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false);
|
||||||
}
|
}
|
||||||
this.removeCallForRoom(payload.room_id);
|
// don't remove the call yet: let the hangup event handler do it (otherwise it will throw
|
||||||
|
// the hangup event away)
|
||||||
break;
|
break;
|
||||||
case 'answer': {
|
case 'answer': {
|
||||||
if (!this.calls.has(payload.room_id)) {
|
if (!this.calls.has(payload.room_id)) {
|
||||||
return; // no call to answer
|
return; // no call to answer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.getAllActiveCalls().length > 1) {
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||||
|
title: _t('Too Many Calls'),
|
||||||
|
description: _t("You've reached the maximum number of simultaneous calls."),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const call = this.calls.get(payload.room_id);
|
const call = this.calls.get(payload.room_id);
|
||||||
call.answer();
|
call.answer();
|
||||||
|
this.setCallAudioElement(call);
|
||||||
|
this.setActiveCallRoomId(payload.room_id);
|
||||||
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
|
@ -561,6 +596,21 @@ export default class CallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveCallRoomId(activeCallRoomId: string) {
|
||||||
|
logger.info("Setting call in room " + activeCallRoomId + " active");
|
||||||
|
|
||||||
|
for (const [roomId, call] of this.calls.entries()) {
|
||||||
|
if (call.state === CallState.Ended) continue;
|
||||||
|
|
||||||
|
if (roomId === activeCallRoomId) {
|
||||||
|
call.setRemoteOnHold(false);
|
||||||
|
} else {
|
||||||
|
logger.info("Holding call in room " + roomId + " because another call is being set active");
|
||||||
|
call.setRemoteOnHold(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async startCallApp(roomId: string, type: string) {
|
private async startCallApp(roomId: string, type: string) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'appsDrawer',
|
action: 'appsDrawer',
|
||||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { ContextMenu, IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
import { ContextMenu, IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import CallHandler from '../../../CallHandler';
|
||||||
|
|
||||||
interface IProps extends IContextMenuProps {
|
interface IProps extends IContextMenuProps {
|
||||||
call: MatrixCall;
|
call: MatrixCall;
|
||||||
|
@ -34,16 +35,23 @@ export default class CallContextMenu extends React.Component<IProps> {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
onHoldUnholdClick = () => {
|
onHoldClick = () => {
|
||||||
this.props.call.setRemoteOnHold(!this.props.call.isRemoteOnHold());
|
this.props.call.setRemoteOnHold(true);
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnholdClick = () => {
|
||||||
|
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
|
||||||
|
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold");
|
const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold");
|
||||||
|
const handler = this.props.call.isRemoteOnHold() ? this.onUnholdClick : this.onHoldClick;
|
||||||
|
|
||||||
return <ContextMenu {...this.props}>
|
return <ContextMenu {...this.props}>
|
||||||
<MenuItem className="mx_CallContextMenu_item" onClick={this.onHoldUnholdClick}>
|
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
||||||
{holdUnholdCaption}
|
{holdUnholdCaption}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</ContextMenu>;
|
</ContextMenu>;
|
||||||
|
|
|
@ -26,9 +26,9 @@ import classNames from 'classnames';
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import CallView from "../voip/CallView";
|
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||||
|
import CallViewForRoom from '../voip/CallViewForRoom';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// js-sdk room object
|
// js-sdk room object
|
||||||
|
@ -166,8 +166,8 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const callView = (
|
const callView = (
|
||||||
<CallView
|
<CallViewForRoom
|
||||||
room={this.props.room}
|
roomId={this.props.room.roomId}
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
maxVideoHeight={this.props.maxHeight}
|
maxVideoHeight={this.props.maxHeight}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -24,7 +24,8 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import PersistentApp from "../elements/PersistentApp";
|
import PersistentApp from "../elements/PersistentApp";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
|
@ -40,9 +41,50 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
activeCall: MatrixCall;
|
|
||||||
|
// The main call that we are displaying (ie. not including the call in the room being viewed, if any)
|
||||||
|
primaryCall: MatrixCall;
|
||||||
|
|
||||||
|
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||||
|
// they belong to
|
||||||
|
secondaryCall: MatrixCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Splits a list of calls into one 'primary' one and a list
|
||||||
|
// (which should be a single element) of other calls.
|
||||||
|
// The primary will be the one not on hold, or an arbitrary one
|
||||||
|
// if they're all on hold)
|
||||||
|
function getPrimarySecondaryCalls(calls: MatrixCall[]): [MatrixCall, MatrixCall[]] {
|
||||||
|
let primary: MatrixCall = null;
|
||||||
|
let secondaries: MatrixCall[] = [];
|
||||||
|
|
||||||
|
for (const call of calls) {
|
||||||
|
if (!SHOW_CALL_IN_STATES.includes(call.state)) continue;
|
||||||
|
|
||||||
|
if (!call.isRemoteOnHold() && primary === null) {
|
||||||
|
primary = call;
|
||||||
|
} else {
|
||||||
|
secondaries.push(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primary === null && secondaries.length > 0) {
|
||||||
|
primary = secondaries[0];
|
||||||
|
secondaries = secondaries.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secondaries.length > 1) {
|
||||||
|
// We should never be in more than two calls so this shouldn't happen
|
||||||
|
console.log("Found more than 1 secondary call! Other calls will not be shown.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [primary, secondaries];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CallPreview shows a small version of CallView hovering over the UI in 'picture-in-picture'
|
||||||
|
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing.
|
||||||
|
*/
|
||||||
export default class CallPreview extends React.Component<IProps, IState> {
|
export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
private roomStoreToken: any;
|
private roomStoreToken: any;
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
|
@ -51,18 +93,27 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const roomId = RoomViewStore.getRoomId();
|
||||||
|
|
||||||
|
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||||
|
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(roomId),
|
||||||
|
);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId,
|
||||||
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
|
primaryCall: primaryCall,
|
||||||
|
secondaryCall: secondaryCalls[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
if (this.roomStoreToken) {
|
if (this.roomStoreToken) {
|
||||||
this.roomStoreToken.remove();
|
this.roomStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -72,8 +123,16 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onRoomViewStoreUpdate = (payload) => {
|
private onRoomViewStoreUpdate = (payload) => {
|
||||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
|
|
||||||
|
const roomId = RoomViewStore.getRoomId();
|
||||||
|
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||||
|
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(roomId),
|
||||||
|
);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId,
|
||||||
|
primaryCall: primaryCall,
|
||||||
|
secondaryCall: secondaryCalls[0],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,38 +140,35 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
// listen for call state changes to prod the render method, which
|
// listen for call state changes to prod the render method, which
|
||||||
// may hide the global CallView if the call it is tracking is dead
|
// may hide the global CallView if the call it is tracking is dead
|
||||||
case 'call_state':
|
case 'call_state': {
|
||||||
|
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||||
|
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||||
|
);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
|
primaryCall: primaryCall,
|
||||||
|
secondaryCall: secondaryCalls[0],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCallViewClick = () => {
|
private onCallRemoteHold = () => {
|
||||||
const call = CallHandler.sharedInstance().getAnyActiveCall();
|
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||||
if (call) {
|
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: call.roomId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
|
|
||||||
const showCall = (
|
|
||||||
this.state.activeCall &&
|
|
||||||
SHOW_CALL_IN_STATES.includes(this.state.activeCall.state) &&
|
|
||||||
!callForRoom
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (showCall) {
|
this.setState({
|
||||||
|
primaryCall: primaryCall,
|
||||||
|
secondaryCall: secondaryCalls[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.primaryCall) {
|
||||||
return (
|
return (
|
||||||
<CallView
|
<CallView call={this.state.primaryCall} secondaryCall={this.state.secondaryCall} pipMode={true} />
|
||||||
onClick={this.onCallViewClick}
|
|
||||||
showHangup={true}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, CSSProperties } from 'react';
|
import React, { createRef, CSSProperties, ReactNode } from 'react';
|
||||||
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';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
@ -33,26 +32,27 @@ import CallContextMenu from '../context_menus/CallContextMenu';
|
||||||
import { avatarUrlForMember } from '../../../Avatar';
|
import { avatarUrlForMember } from '../../../Avatar';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// js-sdk room object. If set, we will only show calls for the given
|
// The call for us to display
|
||||||
// room; if not, we will show any active call.
|
call: MatrixCall,
|
||||||
room?: Room;
|
|
||||||
|
// Another ongoing call to display information about
|
||||||
|
secondaryCall?: MatrixCall,
|
||||||
|
|
||||||
// maxHeight style attribute for the video panel
|
// maxHeight style attribute for the video panel
|
||||||
maxVideoHeight?: number;
|
maxVideoHeight?: number;
|
||||||
|
|
||||||
// a callback which is called when the user clicks on the video div
|
|
||||||
onClick?: React.MouseEventHandler;
|
|
||||||
|
|
||||||
// a callback which is called when the content in the callview changes
|
// a callback which is called when the content in the callview changes
|
||||||
// in a way that is likely to cause a resize.
|
// in a way that is likely to cause a resize.
|
||||||
onResize?: any;
|
onResize?: any;
|
||||||
|
|
||||||
// Whether to show the hang up icon:W
|
// Whether this call view is for picture-in-pictue mode
|
||||||
showHangup?: boolean;
|
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||||
|
// This is sort of a proxy for a number of things but we currently have no
|
||||||
|
// need to control those things separately, so this is simpler.
|
||||||
|
pipMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
call: MatrixCall;
|
|
||||||
isLocalOnHold: boolean,
|
isLocalOnHold: boolean,
|
||||||
isRemoteOnHold: boolean,
|
isRemoteOnHold: boolean,
|
||||||
micMuted: boolean,
|
micMuted: boolean,
|
||||||
|
@ -94,10 +94,8 @@ 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 BOTTOM_PADDING = 10;
|
||||||
// Also duplicated from the CSS: the bottom padding on the call view
|
const BOTTOM_MARGIN_TOP_BOTTOM = 10; // top margin plus bottom margin
|
||||||
const CALL_PADDING_BOTTOM = 10;
|
|
||||||
|
|
||||||
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
|
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> {
|
||||||
|
@ -109,19 +107,17 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const call = this.getCall();
|
|
||||||
this.state = {
|
this.state = {
|
||||||
call,
|
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||||
isLocalOnHold: call ? call.isLocalOnHold() : null,
|
isRemoteOnHold: this.props.call.isRemoteOnHold(),
|
||||||
isRemoteOnHold: call ? call.isRemoteOnHold() : null,
|
micMuted: this.props.call.isMicrophoneMuted(),
|
||||||
micMuted: call ? call.isMicrophoneMuted() : null,
|
vidMuted: this.props.call.isLocalVideoMuted(),
|
||||||
vidMuted: call ? call.isLocalVideoMuted() : null,
|
callState: this.props.call.state,
|
||||||
callState: call ? call.state : null,
|
|
||||||
controlsVisible: true,
|
controlsVisible: true,
|
||||||
showMoreMenu: false,
|
showMoreMenu: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateCallListeners(null, call);
|
this.updateCallListeners(null, this.props.call);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
@ -130,11 +126,29 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
if (getFullScreenElement()) {
|
||||||
|
exitFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
document.removeEventListener("keydown", this.onNativeKeyDown);
|
document.removeEventListener("keydown", this.onNativeKeyDown);
|
||||||
this.updateCallListeners(this.state.call, null);
|
this.updateCallListeners(this.props.call, null);
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.call === prevProps.call) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||||
|
isRemoteOnHold: this.props.call.isRemoteOnHold(),
|
||||||
|
micMuted: this.props.call.isMicrophoneMuted(),
|
||||||
|
vidMuted: this.props.call.isLocalVideoMuted(),
|
||||||
|
callState: this.props.call.state,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateCallListeners(null, this.props.call);
|
||||||
|
}
|
||||||
|
|
||||||
private onAction = (payload) => {
|
private onAction = (payload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'video_fullscreen': {
|
case 'video_fullscreen': {
|
||||||
|
@ -148,85 +162,41 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'call_state': {
|
|
||||||
const newCall = this.getCall();
|
|
||||||
if (newCall !== this.state.call) {
|
|
||||||
this.updateCallListeners(this.state.call, newCall);
|
|
||||||
let newControlsVisible = this.state.controlsVisible;
|
|
||||||
if (newCall && !this.state.call) {
|
|
||||||
newControlsVisible = true;
|
|
||||||
if (this.controlsHideTimer !== null) {
|
|
||||||
clearTimeout(this.controlsHideTimer);
|
|
||||||
}
|
|
||||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
call: newCall,
|
|
||||||
isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
|
|
||||||
isRemoteOnHold: newCall ? newCall.isRemoteOnHold() : null,
|
|
||||||
micMuted: newCall ? newCall.isMicrophoneMuted() : null,
|
|
||||||
vidMuted: newCall ? newCall.isLocalVideoMuted() : null,
|
|
||||||
callState: newCall ? newCall.state : null,
|
|
||||||
controlsVisible: newControlsVisible,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
callState: newCall ? newCall.state : null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!newCall && getFullScreenElement()) {
|
|
||||||
exitFullscreen();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private getCall(): MatrixCall {
|
|
||||||
let call: MatrixCall;
|
|
||||||
|
|
||||||
if (this.props.room) {
|
|
||||||
const roomId = this.props.room.roomId;
|
|
||||||
call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
|
||||||
} else {
|
|
||||||
call = CallHandler.sharedInstance().getAnyActiveCall();
|
|
||||||
// Ignore calls if we can't get the room associated with them.
|
|
||||||
// I think the underlying problem is that the js-sdk sends events
|
|
||||||
// for calls before it has made the rooms available in the store,
|
|
||||||
// although this isn't confirmed.
|
|
||||||
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
|
|
||||||
call = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
|
|
||||||
return call;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
|
private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
|
||||||
if (oldCall === newCall) return;
|
if (oldCall === newCall) return;
|
||||||
|
|
||||||
if (oldCall) {
|
if (oldCall) {
|
||||||
|
oldCall.removeListener(CallEvent.State, this.onCallState);
|
||||||
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||||
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||||
}
|
}
|
||||||
if (newCall) {
|
if (newCall) {
|
||||||
|
newCall.on(CallEvent.State, this.onCallState);
|
||||||
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||||
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onCallState = (state) => {
|
||||||
|
this.setState({
|
||||||
|
callState: state,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onCallLocalHoldUnhold = () => {
|
private onCallLocalHoldUnhold = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLocalOnHold: this.state.call ? this.state.call.isLocalOnHold() : null,
|
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCallRemoteHoldUnhold = () => {
|
private onCallRemoteHoldUnhold = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isRemoteOnHold: this.state.call ? this.state.call.isRemoteOnHold() : null,
|
isRemoteOnHold: this.props.call.isRemoteOnHold(),
|
||||||
// update both here because isLocalOnHold changes when we hold the call too
|
// update both here because isLocalOnHold changes when we hold the call too
|
||||||
isLocalOnHold: this.state.call ? this.state.call.isLocalOnHold() : null,
|
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,7 +210,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
private onExpandClick = () => {
|
private onExpandClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: this.state.call.roomId,
|
room_id: this.props.call.roomId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -270,20 +240,16 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMicMuteClick = () => {
|
private onMicMuteClick = () => {
|
||||||
if (!this.state.call) return;
|
|
||||||
|
|
||||||
const newVal = !this.state.micMuted;
|
const newVal = !this.state.micMuted;
|
||||||
|
|
||||||
this.state.call.setMicrophoneMuted(newVal);
|
this.props.call.setMicrophoneMuted(newVal);
|
||||||
this.setState({micMuted: newVal});
|
this.setState({micMuted: newVal});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onVidMuteClick = () => {
|
private onVidMuteClick = () => {
|
||||||
if (!this.state.call) return;
|
|
||||||
|
|
||||||
const newVal = !this.state.vidMuted;
|
const newVal = !this.state.vidMuted;
|
||||||
|
|
||||||
this.state.call.setLocalVideoMuted(newVal);
|
this.props.call.setLocalVideoMuted(newVal);
|
||||||
this.setState({vidMuted: newVal});
|
this.setState({vidMuted: newVal});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,107 +308,114 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
private onRoomAvatarClick = () => {
|
private onRoomAvatarClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: this.state.call.roomId,
|
room_id: this.props.call.roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSecondaryRoomAvatarClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: this.props.secondaryCall.roomId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCallResumeClick = () => {
|
private onCallResumeClick = () => {
|
||||||
this.state.call.setRemoteOnHold(false);
|
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSecondaryCallResumeClick = () => {
|
||||||
|
CallHandler.sharedInstance().setActiveCallRoomId(this.props.secondaryCall.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
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.props.call.roomId);
|
||||||
|
const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null;
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
|
|
||||||
let callControls;
|
if (this.state.showMoreMenu) {
|
||||||
if (this.props.room) {
|
contextMenu = <CallContextMenu
|
||||||
if (this.state.showMoreMenu) {
|
{...aboveLeftOf(
|
||||||
contextMenu = <CallContextMenu
|
this.contextMenuButton.current.getBoundingClientRect(),
|
||||||
{...aboveLeftOf(
|
ChevronFace.None,
|
||||||
this.contextMenuButton.current.getBoundingClientRect(),
|
CONTEXT_MENU_VPADDING,
|
||||||
ChevronFace.None,
|
)}
|
||||||
CONTEXT_MENU_VPADDING,
|
onFinished={this.closeContextMenu}
|
||||||
)}
|
call={this.props.call}
|
||||||
onFinished={this.closeContextMenu}
|
/>;
|
||||||
call={this.state.call}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const micClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_micOn: !this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_micOff: this.state.micMuted,
|
|
||||||
});
|
|
||||||
|
|
||||||
const vidClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_vidOn: !this.state.vidMuted,
|
|
||||||
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Put the other states of the mic/video icons in the document to make sure they're cached
|
|
||||||
// (otherwise the icon disappears briefly when toggled)
|
|
||||||
const micCacheClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_micOn: this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_micOff: !this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_invisible: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const vidCacheClasses = classNames({
|
|
||||||
mx_CallView_callControls_button: true,
|
|
||||||
mx_CallView_callControls_button_vidOn: this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_vidOff: !this.state.micMuted,
|
|
||||||
mx_CallView_callControls_button_invisible: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const callControlsClasses = classNames({
|
|
||||||
mx_CallView_callControls: true,
|
|
||||||
mx_CallView_callControls_hidden: !this.state.controlsVisible,
|
|
||||||
});
|
|
||||||
|
|
||||||
const vidMuteButton = this.state.call.type === CallType.Video ? <AccessibleButton
|
|
||||||
className={vidClasses}
|
|
||||||
onClick={this.onVidMuteClick}
|
|
||||||
/> : 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}>
|
|
||||||
<div className="mx_CallView_callControls_button mx_CallView_callControls_nothing" />
|
|
||||||
<AccessibleButton
|
|
||||||
className={micClasses}
|
|
||||||
onClick={this.onMicMuteClick}
|
|
||||||
/>
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
|
||||||
onClick={() => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'hangup',
|
|
||||||
room_id: this.state.call.roomId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{vidMuteButton}
|
|
||||||
<div className={micCacheClasses} />
|
|
||||||
<div className={vidCacheClasses} />
|
|
||||||
{contextMenuButton}
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const micClasses = classNames({
|
||||||
|
mx_CallView_callControls_button: true,
|
||||||
|
mx_CallView_callControls_button_micOn: !this.state.micMuted,
|
||||||
|
mx_CallView_callControls_button_micOff: this.state.micMuted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vidClasses = classNames({
|
||||||
|
mx_CallView_callControls_button: true,
|
||||||
|
mx_CallView_callControls_button_vidOn: !this.state.vidMuted,
|
||||||
|
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Put the other states of the mic/video icons in the document to make sure they're cached
|
||||||
|
// (otherwise the icon disappears briefly when toggled)
|
||||||
|
const micCacheClasses = classNames({
|
||||||
|
mx_CallView_callControls_button: true,
|
||||||
|
mx_CallView_callControls_button_micOn: this.state.micMuted,
|
||||||
|
mx_CallView_callControls_button_micOff: !this.state.micMuted,
|
||||||
|
mx_CallView_callControls_button_invisible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vidCacheClasses = classNames({
|
||||||
|
mx_CallView_callControls_button: true,
|
||||||
|
mx_CallView_callControls_button_vidOn: this.state.micMuted,
|
||||||
|
mx_CallView_callControls_button_vidOff: !this.state.micMuted,
|
||||||
|
mx_CallView_callControls_button_invisible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const callControlsClasses = classNames({
|
||||||
|
mx_CallView_callControls: true,
|
||||||
|
mx_CallView_callControls_hidden: !this.state.controlsVisible,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vidMuteButton = this.props.call.type === CallType.Video ? <AccessibleButton
|
||||||
|
className={vidClasses}
|
||||||
|
onClick={this.onVidMuteClick}
|
||||||
|
/> : 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.
|
||||||
|
const callControls = <div className={callControlsClasses}>
|
||||||
|
<div className="mx_CallView_callControls_button mx_CallView_callControls_nothing" />
|
||||||
|
<AccessibleButton
|
||||||
|
className={micClasses}
|
||||||
|
onClick={this.onMicMuteClick}
|
||||||
|
/>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||||
|
onClick={() => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'hangup',
|
||||||
|
room_id: this.props.call.roomId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{vidMuteButton}
|
||||||
|
<div className={micCacheClasses} />
|
||||||
|
<div className={vidCacheClasses} />
|
||||||
|
{contextMenuButton}
|
||||||
|
</div>;
|
||||||
|
|
||||||
// The 'content' for the call, ie. the videos for a video call and profile picture
|
// The 'content' for the call, ie. the videos for a video call and profile picture
|
||||||
// for voice calls (fills the bg)
|
// for voice calls (fills the bg)
|
||||||
let contentView: React.ReactNode;
|
let contentView: React.ReactNode;
|
||||||
|
@ -457,11 +430,11 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
} else if (this.state.isLocalOnHold) {
|
} else if (this.state.isLocalOnHold) {
|
||||||
onHoldText = _t("%(peerName)s held the call", {
|
onHoldText = _t("%(peerName)s held the call", {
|
||||||
peerName: this.state.call.getOpponentMember().name,
|
peerName: this.props.call.getOpponentMember().name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.call.type === CallType.Video) {
|
if (this.props.call.type === CallType.Video) {
|
||||||
let onHoldContent = null;
|
let onHoldContent = null;
|
||||||
let onHoldBackground = null;
|
let onHoldBackground = null;
|
||||||
const backgroundStyle: CSSProperties = {};
|
const backgroundStyle: CSSProperties = {};
|
||||||
|
@ -475,7 +448,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
</div>;
|
</div>;
|
||||||
const backgroundAvatarUrl = avatarUrlForMember(
|
const backgroundAvatarUrl = avatarUrlForMember(
|
||||||
// is it worth getting the size of the div to pass here?
|
// is it worth getting the size of the div to pass here?
|
||||||
this.state.call.getOpponentMember(), 1024, 1024, 'crop',
|
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||||
);
|
);
|
||||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||||
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||||
|
@ -483,48 +456,71 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// 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 : (
|
const maxVideoHeight = getFullScreenElement() ? null : (
|
||||||
this.props.maxVideoHeight - HEADER_HEIGHT - CALL_PADDING_BOTTOM
|
this.props.maxVideoHeight - (HEADER_HEIGHT + BOTTOM_PADDING + BOTTOM_MARGIN_TOP_BOTTOM)
|
||||||
);
|
);
|
||||||
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
contentView = <div className={containerClasses}
|
||||||
|
ref={this.contentRef} onMouseMove={this.onMouseMove}
|
||||||
|
// Put the max height on here too because this div is ended up 4px larger than the content
|
||||||
|
// and is causing it to scroll, and I am genuinely baffled as to why.
|
||||||
|
style={{maxHeight: maxVideoHeight}}
|
||||||
|
>
|
||||||
{onHoldBackground}
|
{onHoldBackground}
|
||||||
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
|
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize}
|
||||||
maxHeight={maxVideoHeight}
|
maxHeight={maxVideoHeight}
|
||||||
/>
|
/>
|
||||||
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
|
<VideoFeed type={VideoFeedType.Local} call={this.props.call} />
|
||||||
{onHoldContent}
|
{onHoldContent}
|
||||||
{callControls}
|
{callControls}
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
const avatarSize = this.props.room ? 160 : 76;
|
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_CallView_voice: true,
|
mx_CallView_voice: true,
|
||||||
mx_CallView_voice_hold: isOnHold,
|
mx_CallView_voice_hold: isOnHold,
|
||||||
});
|
});
|
||||||
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
let secondaryCallAvatar: ReactNode;
|
||||||
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
|
|
||||||
|
if (this.props.secondaryCall) {
|
||||||
|
const secAvatarSize = this.props.pipMode ? 40 : 100;
|
||||||
|
secondaryCallAvatar = <div className="mx_CallView_voice_secondaryAvatarContainer"
|
||||||
|
style={{width: secAvatarSize, height: secAvatarSize}}
|
||||||
|
>
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
room={callRoom}
|
room={secCallRoom}
|
||||||
height={avatarSize}
|
height={secAvatarSize}
|
||||||
width={avatarSize}
|
width={secAvatarSize}
|
||||||
/>
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
||||||
|
<div className="mx_CallView_voice_avatarsContainer">
|
||||||
|
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
|
||||||
|
<RoomAvatar
|
||||||
|
room={callRoom}
|
||||||
|
height={avatarSize}
|
||||||
|
width={avatarSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{secondaryCallAvatar}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
|
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
|
||||||
{callControls}
|
{callControls}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const callTypeText = this.state.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
|
const callTypeText = this.props.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
|
||||||
let myClassName;
|
let myClassName;
|
||||||
|
|
||||||
let fullScreenButton;
|
let fullScreenButton;
|
||||||
if (this.state.call.type === CallType.Video && this.props.room) {
|
if (this.props.call.type === CallType.Video && !this.props.pipMode) {
|
||||||
fullScreenButton = <div className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
fullScreenButton = <div className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
||||||
onClick={this.onFullscreenClick} title={_t("Fill Screen")}
|
onClick={this.onFullscreenClick} title={_t("Fill Screen")}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let expandButton;
|
let expandButton;
|
||||||
if (!this.props.room) {
|
if (this.props.pipMode) {
|
||||||
expandButton = <div className="mx_CallView_header_button mx_CallView_header_button_expand"
|
expandButton = <div className="mx_CallView_header_button mx_CallView_header_button_expand"
|
||||||
onClick={this.onExpandClick} title={_t("Return to call")}
|
onClick={this.onExpandClick} title={_t("Return to call")}
|
||||||
/>;
|
/>;
|
||||||
|
@ -536,7 +532,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
let header: React.ReactNode;
|
let header: React.ReactNode;
|
||||||
if (this.props.room) {
|
if (!this.props.pipMode) {
|
||||||
header = <div className="mx_CallView_header">
|
header = <div className="mx_CallView_header">
|
||||||
<div className="mx_CallView_header_phoneIcon"></div>
|
<div className="mx_CallView_header_phoneIcon"></div>
|
||||||
<span className="mx_CallView_header_callType">{callTypeText}</span>
|
<span className="mx_CallView_header_callType">{callTypeText}</span>
|
||||||
|
@ -544,13 +540,28 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
</div>;
|
</div>;
|
||||||
myClassName = 'mx_CallView_large';
|
myClassName = 'mx_CallView_large';
|
||||||
} else {
|
} else {
|
||||||
|
let secondaryCallInfo;
|
||||||
|
if (this.props.secondaryCall) {
|
||||||
|
secondaryCallInfo = <span className="mx_CallView_header_secondaryCallInfo">
|
||||||
|
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
||||||
|
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
||||||
|
<span className="mx_CallView_secondaryCall_roomName">
|
||||||
|
{_t("%(name)s paused", { name: secCallRoom.name })}
|
||||||
|
</span>
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
header = <div className="mx_CallView_header">
|
header = <div className="mx_CallView_header">
|
||||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
||||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<div>
|
<div className="mx_CallView_header_callInfo">
|
||||||
<div className="mx_CallView_header_roomName">{callRoom.name}</div>
|
<div className="mx_CallView_header_roomName">{callRoom.name}</div>
|
||||||
<div className="mx_CallView_header_callTypeSmall">{callTypeText}</div>
|
<div className="mx_CallView_header_callTypeSmall">
|
||||||
|
{callTypeText}
|
||||||
|
{secondaryCallInfo}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{headerControls}
|
{headerControls}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import React from 'react';
|
||||||
|
import CallHandler from '../../../CallHandler';
|
||||||
|
import CallView from './CallView';
|
||||||
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// What room we should display the call for
|
||||||
|
roomId: string,
|
||||||
|
|
||||||
|
// maxHeight style attribute for the video panel
|
||||||
|
maxVideoHeight?: number;
|
||||||
|
|
||||||
|
// a callback which is called when the content in the callview changes
|
||||||
|
// in a way that is likely to cause a resize.
|
||||||
|
onResize?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
call: MatrixCall,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Wrapper for CallView that always display the call in a given room,
|
||||||
|
* or nothing if there is no call in that room.
|
||||||
|
*/
|
||||||
|
export default class CallViewForRoom extends React.Component<IProps, IState> {
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
call: this.getCall(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAction = (payload) => {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'call_state': {
|
||||||
|
const newCall = this.getCall();
|
||||||
|
if (newCall !== this.state.call) {
|
||||||
|
this.setState({call: newCall});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getCall(): MatrixCall {
|
||||||
|
const call = CallHandler.sharedInstance().getCallForRoom(this.props.roomId);
|
||||||
|
|
||||||
|
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (!this.state.call) return null;
|
||||||
|
|
||||||
|
return <CallView call={this.state.call} pipMode={false}
|
||||||
|
onResize={this.props.onResize} maxVideoHeight={this.props.maxVideoHeight}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,10 +42,12 @@ export default class VideoFeed extends React.Component<IProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.vid.current.addEventListener('resize', this.onResize);
|
this.vid.current.addEventListener('resize', this.onResize);
|
||||||
if (this.props.type === VideoFeedType.Local) {
|
this.setVideoElement();
|
||||||
this.props.call.setLocalVideoElement(this.vid.current);
|
}
|
||||||
} else {
|
|
||||||
this.props.call.setRemoteVideoElement(this.vid.current);
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.call !== prevProps.call) {
|
||||||
|
this.setVideoElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +55,14 @@ export default class VideoFeed extends React.Component<IProps> {
|
||||||
this.vid.current.removeEventListener('resize', this.onResize);
|
this.vid.current.removeEventListener('resize', this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setVideoElement() {
|
||||||
|
if (this.props.type === VideoFeedType.Local) {
|
||||||
|
this.props.call.setLocalVideoElement(this.vid.current);
|
||||||
|
} else {
|
||||||
|
this.props.call.setRemoteVideoElement(this.vid.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onResize = (e) => {
|
onResize = (e) => {
|
||||||
if (this.props.onResize) {
|
if (this.props.onResize) {
|
||||||
this.props.onResize(e);
|
this.props.onResize(e);
|
||||||
|
|
|
@ -54,10 +54,10 @@
|
||||||
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
|
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
|
||||||
"No other application is using the webcam": "No other application is using the webcam",
|
"No other application is using the webcam": "No other application is using the webcam",
|
||||||
"Unable to capture screen": "Unable to capture screen",
|
"Unable to capture screen": "Unable to capture screen",
|
||||||
"Existing Call": "Existing Call",
|
|
||||||
"You are already in a call.": "You are already in a call.",
|
|
||||||
"VoIP is unsupported": "VoIP is unsupported",
|
"VoIP is unsupported": "VoIP is unsupported",
|
||||||
"You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.",
|
"You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.",
|
||||||
|
"Too Many Calls": "Too Many Calls",
|
||||||
|
"You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.",
|
||||||
"You cannot place a call with yourself.": "You cannot place a call with yourself.",
|
"You cannot place a call with yourself.": "You cannot place a call with yourself.",
|
||||||
"Call in Progress": "Call in Progress",
|
"Call in Progress": "Call in Progress",
|
||||||
"A call is currently being placed!": "A call is currently being placed!",
|
"A call is currently being placed!": "A call is currently being placed!",
|
||||||
|
@ -853,6 +853,7 @@
|
||||||
"Voice Call": "Voice 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 paused": "%(name)s paused",
|
||||||
"Unknown caller": "Unknown caller",
|
"Unknown caller": "Unknown caller",
|
||||||
"Incoming voice call": "Incoming voice call",
|
"Incoming voice call": "Incoming voice call",
|
||||||
"Incoming video call": "Incoming video call",
|
"Incoming video call": "Incoming video call",
|
||||||
|
|
Loading…
Reference in New Issue