mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 19:26:04 +03:00
Merge pull request #5248 from matrix-org/dbkr/callhandler_to_ts
Convert CallHandler to typescript
This commit is contained in:
commit
585c7637d6
17 changed files with 516 additions and 853 deletions
5
src/@types/global.d.ts
vendored
5
src/@types/global.d.ts
vendored
|
@ -30,6 +30,7 @@ import {Notifier} from "../Notifier";
|
||||||
import type {Renderer} from "react-dom";
|
import type {Renderer} from "react-dom";
|
||||||
import RightPanelStore from "../stores/RightPanelStore";
|
import RightPanelStore from "../stores/RightPanelStore";
|
||||||
import WidgetStore from "../stores/WidgetStore";
|
import WidgetStore from "../stores/WidgetStore";
|
||||||
|
import CallHandler from "../CallHandler";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -53,6 +54,7 @@ declare global {
|
||||||
mxNotifier: typeof Notifier;
|
mxNotifier: typeof Notifier;
|
||||||
mxRightPanelStore: RightPanelStore;
|
mxRightPanelStore: RightPanelStore;
|
||||||
mxWidgetStore: WidgetStore;
|
mxWidgetStore: WidgetStore;
|
||||||
|
mxCallHandler: CallHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
|
@ -62,6 +64,9 @@ declare global {
|
||||||
|
|
||||||
interface Navigator {
|
interface Navigator {
|
||||||
userLanguage?: string;
|
userLanguage?: string;
|
||||||
|
// https://github.com/Microsoft/TypeScript/issues/19473
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaSession
|
||||||
|
mediaSession: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StorageEstimate {
|
interface StorageEstimate {
|
||||||
|
|
|
@ -1,526 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
Copyright 2019, 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Manages a list of all the currently active calls.
|
|
||||||
*
|
|
||||||
* This handler dispatches when voip calls are added/updated/removed from this list:
|
|
||||||
* {
|
|
||||||
* action: 'call_state'
|
|
||||||
* room_id: <room ID of the call>
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* To know the state of the call, this handler exposes a getter to
|
|
||||||
* obtain the call for a room:
|
|
||||||
* var call = CallHandler.getCall(roomId)
|
|
||||||
* var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
|
|
||||||
*
|
|
||||||
* This handler listens for and handles the following actions:
|
|
||||||
* {
|
|
||||||
* action: 'place_call',
|
|
||||||
* type: 'voice|video',
|
|
||||||
* room_id: <room that the place call button was pressed in>
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* action: 'incoming_call'
|
|
||||||
* call: MatrixCall
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* action: 'hangup'
|
|
||||||
* room_id: <room that the hangup button was pressed in>
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* action: 'answer'
|
|
||||||
* room_id: <room that the answer button was pressed in>
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
|
||||||
import PlatformPeg from './PlatformPeg';
|
|
||||||
import Modal from './Modal';
|
|
||||||
import { _t } from './languageHandler';
|
|
||||||
import Matrix from 'matrix-js-sdk';
|
|
||||||
import dis from './dispatcher/dispatcher';
|
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
|
||||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
|
||||||
import SettingsStore from './settings/SettingsStore';
|
|
||||||
import {generateHumanReadableId} from "./utils/NamingUtils";
|
|
||||||
import {Jitsi} from "./widgets/Jitsi";
|
|
||||||
import {WidgetType} from "./widgets/WidgetType";
|
|
||||||
import {SettingLevel} from "./settings/SettingLevel";
|
|
||||||
import {base32} from "rfc4648";
|
|
||||||
|
|
||||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
|
||||||
|
|
||||||
global.mxCalls = {
|
|
||||||
//room_id: MatrixCall
|
|
||||||
};
|
|
||||||
const calls = global.mxCalls;
|
|
||||||
let ConferenceHandler = null;
|
|
||||||
|
|
||||||
const audioPromises = {};
|
|
||||||
|
|
||||||
function play(audioId) {
|
|
||||||
// TODO: Attach an invisible element for this instead
|
|
||||||
// which listens?
|
|
||||||
const audio = document.getElementById(audioId);
|
|
||||||
if (audio) {
|
|
||||||
const playAudio = async () => {
|
|
||||||
try {
|
|
||||||
// This still causes the chrome debugger to break on promise rejection if
|
|
||||||
// the promise is rejected, even though we're catching the exception.
|
|
||||||
await audio.play();
|
|
||||||
} catch (e) {
|
|
||||||
// This is usually because the user hasn't interacted with the document,
|
|
||||||
// or chrome doesn't think so and is denying the request. Not sure what
|
|
||||||
// we can really do here...
|
|
||||||
// https://github.com/vector-im/element-web/issues/7657
|
|
||||||
console.log("Unable to play audio clip", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (audioPromises[audioId]) {
|
|
||||||
audioPromises[audioId] = audioPromises[audioId].then(()=>{
|
|
||||||
audio.load();
|
|
||||||
return playAudio();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
audioPromises[audioId] = playAudio();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pause(audioId) {
|
|
||||||
// TODO: Attach an invisible element for this instead
|
|
||||||
// which listens?
|
|
||||||
const audio = document.getElementById(audioId);
|
|
||||||
if (audio) {
|
|
||||||
if (audioPromises[audioId]) {
|
|
||||||
audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause());
|
|
||||||
} else {
|
|
||||||
// pause doesn't actually return a promise, but might as well do this for symmetry with play();
|
|
||||||
audioPromises[audioId] = audio.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _setCallListeners(call) {
|
|
||||||
call.on("error", function(err) {
|
|
||||||
console.error("Call error:", err);
|
|
||||||
if (
|
|
||||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
|
||||||
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
|
||||||
) {
|
|
||||||
_showICEFallbackPrompt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
|
||||||
title: _t('Call Failed'),
|
|
||||||
description: err.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
call.on("hangup", function() {
|
|
||||||
_setCallState(undefined, call.roomId, "ended");
|
|
||||||
});
|
|
||||||
// map web rtc states to dummy UI state
|
|
||||||
// ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
|
|
||||||
call.on("state", function(newState, oldState) {
|
|
||||||
if (newState === "ringing") {
|
|
||||||
_setCallState(call, call.roomId, "ringing");
|
|
||||||
pause("ringbackAudio");
|
|
||||||
} else if (newState === "invite_sent") {
|
|
||||||
_setCallState(call, call.roomId, "ringback");
|
|
||||||
play("ringbackAudio");
|
|
||||||
} else if (newState === "ended" && oldState === "connected") {
|
|
||||||
_setCallState(undefined, call.roomId, "ended");
|
|
||||||
pause("ringbackAudio");
|
|
||||||
play("callendAudio");
|
|
||||||
} else if (newState === "ended" && oldState === "invite_sent" &&
|
|
||||||
(call.hangupParty === "remote" ||
|
|
||||||
(call.hangupParty === "local" && call.hangupReason === "invite_timeout")
|
|
||||||
)) {
|
|
||||||
_setCallState(call, call.roomId, "busy");
|
|
||||||
pause("ringbackAudio");
|
|
||||||
play("busyAudio");
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
|
||||||
title: _t('Call Timeout'),
|
|
||||||
description: _t('The remote side failed to pick up') + '.',
|
|
||||||
});
|
|
||||||
} else if (oldState === "invite_sent") {
|
|
||||||
_setCallState(call, call.roomId, "stop_ringback");
|
|
||||||
pause("ringbackAudio");
|
|
||||||
} else if (oldState === "ringing") {
|
|
||||||
_setCallState(call, call.roomId, "stop_ringing");
|
|
||||||
pause("ringbackAudio");
|
|
||||||
} else if (newState === "connected") {
|
|
||||||
_setCallState(call, call.roomId, "connected");
|
|
||||||
pause("ringbackAudio");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _setCallState(call, roomId, status) {
|
|
||||||
console.log(
|
|
||||||
`Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
|
|
||||||
);
|
|
||||||
calls[roomId] = call;
|
|
||||||
|
|
||||||
if (status === "ringing") {
|
|
||||||
play("ringAudio");
|
|
||||||
} else if (call && call.call_state === "ringing") {
|
|
||||||
pause("ringAudio");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call) {
|
|
||||||
call.call_state = status;
|
|
||||||
}
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'call_state',
|
|
||||||
room_id: roomId,
|
|
||||||
state: status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _showICEFallbackPrompt() {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const code = sub => <code>{sub}</code>;
|
|
||||||
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
|
|
||||||
title: _t("Call failed due to misconfigured server"),
|
|
||||||
description: <div>
|
|
||||||
<p>{_t(
|
|
||||||
"Please ask the administrator of your homeserver " +
|
|
||||||
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
|
|
||||||
"order for calls to work reliably.",
|
|
||||||
{ homeserverDomain: cli.getDomain() }, { code },
|
|
||||||
)}</p>
|
|
||||||
<p>{_t(
|
|
||||||
"Alternatively, you can try to use the public server at " +
|
|
||||||
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
|
|
||||||
"it will share your IP address with that server. You can also manage " +
|
|
||||||
"this in Settings.",
|
|
||||||
null, { code },
|
|
||||||
)}</p>
|
|
||||||
</div>,
|
|
||||||
button: _t('Try using turn.matrix.org'),
|
|
||||||
cancelButton: _t('OK'),
|
|
||||||
onFinished: (allow) => {
|
|
||||||
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
|
|
||||||
cli.setFallbackICEServerAllowed(allow);
|
|
||||||
},
|
|
||||||
}, null, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _onAction(payload) {
|
|
||||||
function placeCall(newCall) {
|
|
||||||
_setCallListeners(newCall);
|
|
||||||
if (payload.type === 'voice') {
|
|
||||||
newCall.placeVoiceCall();
|
|
||||||
} else if (payload.type === 'video') {
|
|
||||||
newCall.placeVideoCall(
|
|
||||||
payload.remote_element,
|
|
||||||
payload.local_element,
|
|
||||||
);
|
|
||||||
} else if (payload.type === 'screensharing') {
|
|
||||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
|
||||||
if (screenCapErrorString) {
|
|
||||||
_setCallState(undefined, newCall.roomId, "ended");
|
|
||||||
console.log("Can't capture screen: " + screenCapErrorString);
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
|
|
||||||
title: _t('Unable to capture screen'),
|
|
||||||
description: screenCapErrorString,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
newCall.placeScreenSharingCall(
|
|
||||||
payload.remote_element,
|
|
||||||
payload.local_element,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error("Unknown conf call type: %s", payload.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (payload.action) {
|
|
||||||
case 'place_call':
|
|
||||||
{
|
|
||||||
if (callHandler.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 (!MatrixClientPeg.get().supportsVoip()) {
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
|
||||||
title: _t('VoIP is unsupported'),
|
|
||||||
description: _t('You cannot place VoIP calls in this browser.'),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
|
||||||
if (!room) {
|
|
||||||
console.error("Room %s does not exist.", payload.room_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = room.getJoinedMembers();
|
|
||||||
if (members.length <= 1) {
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
|
||||||
description: _t('You cannot place a call with yourself.'),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else if (members.length === 2) {
|
|
||||||
console.info("Place %s call in %s", payload.type, payload.room_id);
|
|
||||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
|
||||||
placeCall(call);
|
|
||||||
} else { // > 2
|
|
||||||
dis.dispatch({
|
|
||||||
action: "place_conference_call",
|
|
||||||
room_id: payload.room_id,
|
|
||||||
type: payload.type,
|
|
||||||
remote_element: payload.remote_element,
|
|
||||||
local_element: payload.local_element,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'place_conference_call':
|
|
||||||
console.info("Place conference call in %s", payload.room_id);
|
|
||||||
_startCallApp(payload.room_id, payload.type);
|
|
||||||
break;
|
|
||||||
case 'incoming_call':
|
|
||||||
{
|
|
||||||
if (callHandler.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 (!MatrixClientPeg.get().supportsVoip()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const call = payload.call;
|
|
||||||
_setCallListeners(call);
|
|
||||||
_setCallState(call, call.roomId, "ringing");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'hangup':
|
|
||||||
if (!calls[payload.room_id]) {
|
|
||||||
return; // no call to hangup
|
|
||||||
}
|
|
||||||
calls[payload.room_id].hangup();
|
|
||||||
_setCallState(null, payload.room_id, "ended");
|
|
||||||
break;
|
|
||||||
case 'answer':
|
|
||||||
if (!calls[payload.room_id]) {
|
|
||||||
return; // no call to answer
|
|
||||||
}
|
|
||||||
calls[payload.room_id].answer();
|
|
||||||
_setCallState(calls[payload.room_id], payload.room_id, "connected");
|
|
||||||
dis.dispatch({
|
|
||||||
action: "view_room",
|
|
||||||
room_id: payload.room_id,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _startCallApp(roomId, type) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'appsDrawer',
|
|
||||||
show: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
|
||||||
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
|
|
||||||
|
|
||||||
if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
|
|
||||||
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
|
|
||||||
title: _t('Call in Progress'),
|
|
||||||
description: _t('A call is currently being placed!'),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentJitsiWidgets.length > 0) {
|
|
||||||
console.warn(
|
|
||||||
"Refusing to start conference call widget in " + roomId +
|
|
||||||
" a conference call widget is already present",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (WidgetUtils.canUserModifyWidgets(roomId)) {
|
|
||||||
Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
|
|
||||||
title: _t('End Call'),
|
|
||||||
description: _t('Remove the group call from the room?'),
|
|
||||||
button: _t('End Call'),
|
|
||||||
cancelButton: _t('Cancel'),
|
|
||||||
onFinished: (endCall) => {
|
|
||||||
if (endCall) {
|
|
||||||
WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
|
|
||||||
title: _t('Call in Progress'),
|
|
||||||
description: _t("You don't have permission to remove the call from the room"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jitsiDomain = Jitsi.getInstance().preferredDomain;
|
|
||||||
const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
|
|
||||||
let confId;
|
|
||||||
if (jitsiAuth === 'openidtoken-jwt') {
|
|
||||||
// Create conference ID from room ID
|
|
||||||
// For compatibility with Jitsi, use base32 without padding.
|
|
||||||
// More details here:
|
|
||||||
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
|
||||||
confId = base32.stringify(Buffer.from(roomId), { pad: false });
|
|
||||||
} else {
|
|
||||||
// Create a random human readable conference ID
|
|
||||||
confId = `JitsiConference${generateHumanReadableId()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
|
|
||||||
|
|
||||||
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
|
|
||||||
const parsedUrl = new URL(widgetUrl);
|
|
||||||
parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead
|
|
||||||
parsedUrl.searchParams.set('confId', confId);
|
|
||||||
widgetUrl = parsedUrl.toString();
|
|
||||||
|
|
||||||
const widgetData = {
|
|
||||||
conferenceId: confId,
|
|
||||||
isAudioOnly: type === 'voice',
|
|
||||||
domain: jitsiDomain,
|
|
||||||
auth: jitsiAuth,
|
|
||||||
};
|
|
||||||
|
|
||||||
const widgetId = (
|
|
||||||
'jitsi_' +
|
|
||||||
MatrixClientPeg.get().credentials.userId +
|
|
||||||
'_' +
|
|
||||||
Date.now()
|
|
||||||
);
|
|
||||||
|
|
||||||
WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
|
|
||||||
console.log('Jitsi widget added');
|
|
||||||
}).catch((e) => {
|
|
||||||
if (e.errcode === 'M_FORBIDDEN') {
|
|
||||||
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
|
||||||
title: _t('Permission Required'),
|
|
||||||
description: _t("You do not have permission to start a conference call in this room"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Nasty way of making sure we only register
|
|
||||||
// with the dispatcher once
|
|
||||||
if (!global.mxCallHandler) {
|
|
||||||
dis.register(_onAction);
|
|
||||||
// add empty handlers for media actions, otherwise the media keys
|
|
||||||
// end up causing the audio elements with our ring/ringback etc
|
|
||||||
// audio clips in to play.
|
|
||||||
if (navigator.mediaSession) {
|
|
||||||
navigator.mediaSession.setActionHandler('play', function() {});
|
|
||||||
navigator.mediaSession.setActionHandler('pause', function() {});
|
|
||||||
navigator.mediaSession.setActionHandler('seekbackward', function() {});
|
|
||||||
navigator.mediaSession.setActionHandler('seekforward', function() {});
|
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', function() {});
|
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', function() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const callHandler = {
|
|
||||||
getCallForRoom: function(roomId) {
|
|
||||||
let call = callHandler.getCall(roomId);
|
|
||||||
if (call) return call;
|
|
||||||
|
|
||||||
if (ConferenceHandler) {
|
|
||||||
call = ConferenceHandler.getConferenceCallForRoom(roomId);
|
|
||||||
}
|
|
||||||
if (call) return call;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
getCall: function(roomId) {
|
|
||||||
return calls[roomId] || null;
|
|
||||||
},
|
|
||||||
|
|
||||||
getAnyActiveCall: function() {
|
|
||||||
const roomsWithCalls = Object.keys(calls);
|
|
||||||
for (let i = 0; i < roomsWithCalls.length; i++) {
|
|
||||||
if (calls[roomsWithCalls[i]] &&
|
|
||||||
calls[roomsWithCalls[i]].call_state !== "ended") {
|
|
||||||
return calls[roomsWithCalls[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The conference handler is a module that deals with implementation-specific
|
|
||||||
* multi-party calling implementations. Element passes in its own which creates
|
|
||||||
* a one-to-one call with a freeswitch conference bridge. As of July 2018,
|
|
||||||
* the de-facto way of conference calling is a Jitsi widget, so this is
|
|
||||||
* deprecated. It reamins here for two reasons:
|
|
||||||
* 1. So Element still supports joining existing freeswitch conference calls
|
|
||||||
* (but doesn't support creating them). After a transition period, we can
|
|
||||||
* remove support for joining them too.
|
|
||||||
* 2. To hide the one-to-one rooms that old-style conferencing creates. This
|
|
||||||
* is much harder to remove: probably either we make Element leave & forget these
|
|
||||||
* rooms after we remove support for joining freeswitch conferences, or we
|
|
||||||
* accept that random rooms with cryptic users will suddently appear for
|
|
||||||
* anyone who's ever used conference calling, or we are stuck with this
|
|
||||||
* code forever.
|
|
||||||
*
|
|
||||||
* @param {object} confHandler The conference handler object
|
|
||||||
*/
|
|
||||||
setConferenceHandler: function(confHandler) {
|
|
||||||
ConferenceHandler = confHandler;
|
|
||||||
},
|
|
||||||
|
|
||||||
getConferenceHandler: function() {
|
|
||||||
return ConferenceHandler;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Only things in here which actually need to be global are the
|
|
||||||
// calls list (done separately) and making sure we only register
|
|
||||||
// with the dispatcher once (which uses this mechanism but checks
|
|
||||||
// separately). This could be tidied up.
|
|
||||||
if (global.mxCallHandler === undefined) {
|
|
||||||
global.mxCallHandler = callHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default global.mxCallHandler;
|
|
487
src/CallHandler.tsx
Normal file
487
src/CallHandler.tsx
Normal file
|
@ -0,0 +1,487 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
Copyright 2019, 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Manages a list of all the currently active calls.
|
||||||
|
*
|
||||||
|
* This handler dispatches when voip calls are added/updated/removed from this list:
|
||||||
|
* {
|
||||||
|
* action: 'call_state'
|
||||||
|
* room_id: <room ID of the call>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* To know the state of the call, this handler exposes a getter to
|
||||||
|
* obtain the call for a room:
|
||||||
|
* var call = CallHandler.getCall(roomId)
|
||||||
|
* var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
|
||||||
|
*
|
||||||
|
* This handler listens for and handles the following actions:
|
||||||
|
* {
|
||||||
|
* action: 'place_call',
|
||||||
|
* type: 'voice|video',
|
||||||
|
* room_id: <room that the place call button was pressed in>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* action: 'incoming_call'
|
||||||
|
* call: MatrixCall
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* action: 'hangup'
|
||||||
|
* room_id: <room that the hangup button was pressed in>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* action: 'answer'
|
||||||
|
* room_id: <room that the answer button was pressed in>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
|
import PlatformPeg from './PlatformPeg';
|
||||||
|
import Modal from './Modal';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||||
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
import dis from './dispatcher/dispatcher';
|
||||||
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
|
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||||
|
import SettingsStore from './settings/SettingsStore';
|
||||||
|
import {generateHumanReadableId} from "./utils/NamingUtils";
|
||||||
|
import {Jitsi} from "./widgets/Jitsi";
|
||||||
|
import {WidgetType} from "./widgets/WidgetType";
|
||||||
|
import {SettingLevel} from "./settings/SettingLevel";
|
||||||
|
import { ActionPayload } from "./dispatcher/payloads";
|
||||||
|
import {base32} from "rfc4648";
|
||||||
|
|
||||||
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||||
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
|
|
||||||
|
// until we ts-ify the js-sdk voip code
|
||||||
|
type Call = any;
|
||||||
|
|
||||||
|
export default class CallHandler {
|
||||||
|
private calls = new Map<string, Call>();
|
||||||
|
private audioPromises = new Map<string, Promise<void>>();
|
||||||
|
|
||||||
|
static sharedInstance() {
|
||||||
|
if (!window.mxCallHandler) {
|
||||||
|
window.mxCallHandler = new CallHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.mxCallHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
dis.register(this.onAction);
|
||||||
|
// add empty handlers for media actions, otherwise the media keys
|
||||||
|
// end up causing the audio elements with our ring/ringback etc
|
||||||
|
// audio clips in to play.
|
||||||
|
if (navigator.mediaSession) {
|
||||||
|
navigator.mediaSession.setActionHandler('play', function() {});
|
||||||
|
navigator.mediaSession.setActionHandler('pause', function() {});
|
||||||
|
navigator.mediaSession.setActionHandler('seekbackward', function() {});
|
||||||
|
navigator.mediaSession.setActionHandler('seekforward', function() {});
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', function() {});
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', function() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCallForRoom(roomId: string): Call {
|
||||||
|
return this.calls.get(roomId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAnyActiveCall() {
|
||||||
|
const roomsWithCalls = Object.keys(this.calls);
|
||||||
|
for (let i = 0; i < roomsWithCalls.length; i++) {
|
||||||
|
if (this.calls.get(roomsWithCalls[i]) &&
|
||||||
|
this.calls.get(roomsWithCalls[i]).call_state !== "ended") {
|
||||||
|
return this.calls.get(roomsWithCalls[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
play(audioId: string) {
|
||||||
|
// TODO: Attach an invisible element for this instead
|
||||||
|
// which listens?
|
||||||
|
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||||
|
if (audio) {
|
||||||
|
const playAudio = async () => {
|
||||||
|
try {
|
||||||
|
// This still causes the chrome debugger to break on promise rejection if
|
||||||
|
// the promise is rejected, even though we're catching the exception.
|
||||||
|
await audio.play();
|
||||||
|
} catch (e) {
|
||||||
|
// This is usually because the user hasn't interacted with the document,
|
||||||
|
// or chrome doesn't think so and is denying the request. Not sure what
|
||||||
|
// we can really do here...
|
||||||
|
// https://github.com/vector-im/element-web/issues/7657
|
||||||
|
console.log("Unable to play audio clip", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.audioPromises.has(audioId)) {
|
||||||
|
this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => {
|
||||||
|
audio.load();
|
||||||
|
return playAudio();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.audioPromises.set(audioId, playAudio());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pause(audioId: string) {
|
||||||
|
// TODO: Attach an invisible element for this instead
|
||||||
|
// which listens?
|
||||||
|
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||||
|
if (audio) {
|
||||||
|
if (this.audioPromises.has(audioId)) {
|
||||||
|
this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => audio.pause()));
|
||||||
|
} else {
|
||||||
|
// pause doesn't return a promise, so just do it
|
||||||
|
audio.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCallListeners(call: Call) {
|
||||||
|
call.on("error", (err) => {
|
||||||
|
console.error("Call error:", err);
|
||||||
|
if (
|
||||||
|
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||||
|
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
||||||
|
) {
|
||||||
|
this.showICEFallbackPrompt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
||||||
|
title: _t('Call Failed'),
|
||||||
|
description: err.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
call.on("hangup", () => {
|
||||||
|
this.setCallState(undefined, call.roomId, "ended");
|
||||||
|
});
|
||||||
|
// map web rtc states to dummy UI state
|
||||||
|
// ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
|
||||||
|
call.on("state", (newState, oldState) => {
|
||||||
|
if (newState === "ringing") {
|
||||||
|
this.setCallState(call, call.roomId, "ringing");
|
||||||
|
this.pause("ringbackAudio");
|
||||||
|
} else if (newState === "invite_sent") {
|
||||||
|
this.setCallState(call, call.roomId, "ringback");
|
||||||
|
this.play("ringbackAudio");
|
||||||
|
} else if (newState === "ended" && oldState === "connected") {
|
||||||
|
this.setCallState(undefined, call.roomId, "ended");
|
||||||
|
this.pause("ringbackAudio");
|
||||||
|
this.play("callendAudio");
|
||||||
|
} else if (newState === "ended" && oldState === "invite_sent" &&
|
||||||
|
(call.hangupParty === "remote" ||
|
||||||
|
(call.hangupParty === "local" && call.hangupReason === "invite_timeout")
|
||||||
|
)) {
|
||||||
|
this.setCallState(call, call.roomId, "busy");
|
||||||
|
this.pause("ringbackAudio");
|
||||||
|
this.play("busyAudio");
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
||||||
|
title: _t('Call Timeout'),
|
||||||
|
description: _t('The remote side failed to pick up') + '.',
|
||||||
|
});
|
||||||
|
} else if (oldState === "invite_sent") {
|
||||||
|
this.setCallState(call, call.roomId, "stop_ringback");
|
||||||
|
this.pause("ringbackAudio");
|
||||||
|
} else if (oldState === "ringing") {
|
||||||
|
this.setCallState(call, call.roomId, "stop_ringing");
|
||||||
|
this.pause("ringbackAudio");
|
||||||
|
} else if (newState === "connected") {
|
||||||
|
this.setCallState(call, call.roomId, "connected");
|
||||||
|
this.pause("ringbackAudio");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCallState(call: Call, roomId: string, status: string) {
|
||||||
|
console.log(
|
||||||
|
`Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
|
||||||
|
);
|
||||||
|
this.calls.set(roomId, call);
|
||||||
|
|
||||||
|
if (status === "ringing") {
|
||||||
|
this.play("ringAudio");
|
||||||
|
} else if (call && call.call_state === "ringing") {
|
||||||
|
this.pause("ringAudio");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call) {
|
||||||
|
call.call_state = status;
|
||||||
|
}
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'call_state',
|
||||||
|
room_id: roomId,
|
||||||
|
state: status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private showICEFallbackPrompt() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const code = sub => <code>{sub}</code>;
|
||||||
|
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
|
||||||
|
title: _t("Call failed due to misconfigured server"),
|
||||||
|
description: <div>
|
||||||
|
<p>{_t(
|
||||||
|
"Please ask the administrator of your homeserver " +
|
||||||
|
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
|
||||||
|
"order for calls to work reliably.",
|
||||||
|
{ homeserverDomain: cli.getDomain() }, { code },
|
||||||
|
)}</p>
|
||||||
|
<p>{_t(
|
||||||
|
"Alternatively, you can try to use the public server at " +
|
||||||
|
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
|
||||||
|
"it will share your IP address with that server. You can also manage " +
|
||||||
|
"this in Settings.",
|
||||||
|
null, { code },
|
||||||
|
)}</p>
|
||||||
|
</div>,
|
||||||
|
button: _t('Try using turn.matrix.org'),
|
||||||
|
cancelButton: _t('OK'),
|
||||||
|
onFinished: (allow) => {
|
||||||
|
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
|
||||||
|
cli.setFallbackICEServerAllowed(allow);
|
||||||
|
},
|
||||||
|
}, null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
const placeCall = (newCall) => {
|
||||||
|
this.setCallListeners(newCall);
|
||||||
|
if (payload.type === 'voice') {
|
||||||
|
newCall.placeVoiceCall();
|
||||||
|
} else if (payload.type === 'video') {
|
||||||
|
newCall.placeVideoCall(
|
||||||
|
payload.remote_element,
|
||||||
|
payload.local_element,
|
||||||
|
);
|
||||||
|
} else if (payload.type === 'screensharing') {
|
||||||
|
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||||
|
if (screenCapErrorString) {
|
||||||
|
this.setCallState(undefined, newCall.roomId, "ended");
|
||||||
|
console.log("Can't capture screen: " + screenCapErrorString);
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
|
||||||
|
title: _t('Unable to capture screen'),
|
||||||
|
description: screenCapErrorString,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newCall.placeScreenSharingCall(
|
||||||
|
payload.remote_element,
|
||||||
|
payload.local_element,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error("Unknown conf call type: %s", payload.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payload.action) {
|
||||||
|
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 (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
|
title: _t('VoIP is unsupported'),
|
||||||
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||||
|
if (!room) {
|
||||||
|
console.error("Room %s does not exist.", payload.room_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = room.getJoinedMembers();
|
||||||
|
if (members.length <= 1) {
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
||||||
|
description: _t('You cannot place a call with yourself.'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (members.length === 2) {
|
||||||
|
console.info("Place %s call in %s", payload.type, payload.room_id);
|
||||||
|
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
||||||
|
placeCall(call);
|
||||||
|
} else { // > 2
|
||||||
|
dis.dispatch({
|
||||||
|
action: "place_conference_call",
|
||||||
|
room_id: payload.room_id,
|
||||||
|
type: payload.type,
|
||||||
|
remote_element: payload.remote_element,
|
||||||
|
local_element: payload.local_element,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'place_conference_call':
|
||||||
|
console.info("Place conference call in %s", payload.room_id);
|
||||||
|
this.startCallApp(payload.room_id, payload.type);
|
||||||
|
break;
|
||||||
|
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 (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = payload.call;
|
||||||
|
this.setCallListeners(call);
|
||||||
|
this.setCallState(call, call.roomId, "ringing");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'hangup':
|
||||||
|
if (!this.calls.get(payload.room_id)) {
|
||||||
|
return; // no call to hangup
|
||||||
|
}
|
||||||
|
this.calls.get(payload.room_id).hangup();
|
||||||
|
this.setCallState(null, payload.room_id, "ended");
|
||||||
|
break;
|
||||||
|
case 'answer':
|
||||||
|
if (!this.calls.get(payload.room_id)) {
|
||||||
|
return; // no call to answer
|
||||||
|
}
|
||||||
|
this.calls.get(payload.room_id).answer();
|
||||||
|
this.setCallState(this.calls.get(payload.room_id), payload.room_id, "connected");
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: payload.room_id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startCallApp(roomId: string, type: string) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
|
||||||
|
|
||||||
|
if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
|
||||||
|
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
|
||||||
|
title: _t('Call in Progress'),
|
||||||
|
description: _t('A call is currently being placed!'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentJitsiWidgets.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
"Refusing to start conference call widget in " + roomId +
|
||||||
|
" a conference call widget is already present",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (WidgetUtils.canUserModifyWidgets(roomId)) {
|
||||||
|
Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
|
||||||
|
title: _t('End Call'),
|
||||||
|
description: _t('Remove the group call from the room?'),
|
||||||
|
button: _t('End Call'),
|
||||||
|
cancelButton: _t('Cancel'),
|
||||||
|
onFinished: (endCall) => {
|
||||||
|
if (endCall) {
|
||||||
|
WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
|
||||||
|
title: _t('Call in Progress'),
|
||||||
|
description: _t("You don't have permission to remove the call from the room"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jitsiDomain = Jitsi.getInstance().preferredDomain;
|
||||||
|
const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
|
||||||
|
let confId;
|
||||||
|
if (jitsiAuth === 'openidtoken-jwt') {
|
||||||
|
// Create conference ID from room ID
|
||||||
|
// For compatibility with Jitsi, use base32 without padding.
|
||||||
|
// More details here:
|
||||||
|
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||||
|
confId = base32.stringify(Buffer.from(roomId), { pad: false });
|
||||||
|
} else {
|
||||||
|
// Create a random human readable conference ID
|
||||||
|
confId = `JitsiConference${generateHumanReadableId()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
|
||||||
|
|
||||||
|
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
|
||||||
|
const parsedUrl = new URL(widgetUrl);
|
||||||
|
parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead
|
||||||
|
parsedUrl.searchParams.set('confId', confId);
|
||||||
|
widgetUrl = parsedUrl.toString();
|
||||||
|
|
||||||
|
const widgetData = {
|
||||||
|
conferenceId: confId,
|
||||||
|
isAudioOnly: type === 'voice',
|
||||||
|
domain: jitsiDomain,
|
||||||
|
auth: jitsiAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
const widgetId = (
|
||||||
|
'jitsi_' +
|
||||||
|
MatrixClientPeg.get().credentials.userId +
|
||||||
|
'_' +
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
|
||||||
|
console.log('Jitsi widget added');
|
||||||
|
}).catch((e) => {
|
||||||
|
if (e.errcode === 'M_FORBIDDEN') {
|
||||||
|
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
||||||
|
title: _t('Permission Required'),
|
||||||
|
description: _t("You do not have permission to start a conference call in this room"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
52
src/Rooms.js
52
src/Rooms.js
|
@ -26,58 +26,6 @@ export function getDisplayAliasForRoom(room) {
|
||||||
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If the room contains only two members including the logged-in user,
|
|
||||||
* return the other one. Otherwise, return null.
|
|
||||||
*/
|
|
||||||
export function getOnlyOtherMember(room, myUserId) {
|
|
||||||
if (room.currentState.getJoinedMemberCount() === 2) {
|
|
||||||
return room.getJoinedMembers().filter(function(m) {
|
|
||||||
return m.userId !== myUserId;
|
|
||||||
})[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _isConfCallRoom(room, myUserId, conferenceHandler) {
|
|
||||||
if (!conferenceHandler) return false;
|
|
||||||
|
|
||||||
const myMembership = room.getMyMembership();
|
|
||||||
if (myMembership != "join") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const otherMember = getOnlyOtherMember(room, myUserId);
|
|
||||||
if (!otherMember) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conferenceHandler.isConferenceUser(otherMember.userId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache whether a room is a conference call. Assumes that rooms will always
|
|
||||||
// either will or will not be a conference call room.
|
|
||||||
const isConfCallRoomCache = {
|
|
||||||
// $roomId: bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isConfCallRoom(room, myUserId, conferenceHandler) {
|
|
||||||
if (isConfCallRoomCache[room.roomId] !== undefined) {
|
|
||||||
return isConfCallRoomCache[room.roomId];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = _isConfCallRoom(room, myUserId, conferenceHandler);
|
|
||||||
|
|
||||||
isConfCallRoomCache[room.roomId] = result;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function looksLikeDirectMessageRoom(room, myUserId) {
|
export function looksLikeDirectMessageRoom(room, myUserId) {
|
||||||
const myMembership = room.getMyMembership();
|
const myMembership = room.getMyMembership();
|
||||||
const me = room.getMember(myUserId);
|
const me = room.getMember(myUserId);
|
||||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import CallHandler from './CallHandler';
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
import * as Roles from './Roles';
|
||||||
import {isValid3pidInvite} from "./RoomInvite";
|
import {isValid3pidInvite} from "./RoomInvite";
|
||||||
|
@ -29,7 +28,6 @@ function textForMemberEvent(ev) {
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
const content = ev.getContent();
|
const content = ev.getContent();
|
||||||
|
|
||||||
const ConferenceHandler = CallHandler.getConferenceHandler();
|
|
||||||
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||||
switch (content.membership) {
|
switch (content.membership) {
|
||||||
case 'invite': {
|
case 'invite': {
|
||||||
|
@ -44,11 +42,7 @@ function textForMemberEvent(ev) {
|
||||||
return _t('%(targetName)s accepted an invitation.', {targetName});
|
return _t('%(targetName)s accepted an invitation.', {targetName});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||||
return _t('%(senderName)s requested a VoIP conference.', {senderName});
|
|
||||||
} else {
|
|
||||||
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
|
@ -85,17 +79,11 @@ function textForMemberEvent(ev) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
return _t('%(targetName)s joined the room.', {targetName});
|
||||||
return _t('VoIP conference started.');
|
|
||||||
} else {
|
|
||||||
return _t('%(targetName)s joined the room.', {targetName});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (prevContent.membership === "invite") {
|
||||||
return _t('VoIP conference finished.');
|
|
||||||
} else if (prevContent.membership === "invite") {
|
|
||||||
return _t('%(targetName)s rejected the invitation.', {targetName});
|
return _t('%(targetName)s rejected the invitation.', {targetName});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(targetName)s left the room.', {targetName});
|
return _t('%(targetName)s left the room.', {targetName});
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 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 {createNewMatrixCall as jsCreateNewMatrixCall, Room} from "matrix-js-sdk";
|
|
||||||
import CallHandler from './CallHandler';
|
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
|
||||||
|
|
||||||
// FIXME: this is Element specific code, but will be removed shortly when we
|
|
||||||
// switch over to Jitsi entirely for video conferencing.
|
|
||||||
|
|
||||||
// FIXME: This currently forces Element to try to hit the matrix.org AS for
|
|
||||||
// conferencing. This is bad because it prevents people running their own ASes
|
|
||||||
// from being used. This isn't permanent and will be customisable in the future:
|
|
||||||
// see the proposal at docs/conferencing.md for more info.
|
|
||||||
const USER_PREFIX = "fs_";
|
|
||||||
const DOMAIN = "matrix.org";
|
|
||||||
|
|
||||||
export function ConferenceCall(matrixClient, groupChatRoomId) {
|
|
||||||
this.client = matrixClient;
|
|
||||||
this.groupRoomId = groupChatRoomId;
|
|
||||||
this.confUserId = getConferenceUserIdForRoom(this.groupRoomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConferenceCall.prototype.setup = function() {
|
|
||||||
const self = this;
|
|
||||||
return this._joinConferenceUser().then(function() {
|
|
||||||
return self._getConferenceUserRoom();
|
|
||||||
}).then(function(room) {
|
|
||||||
// return a call for *this* room to be placed. We also tack on
|
|
||||||
// confUserId to speed up lookups (else we'd need to loop every room
|
|
||||||
// looking for a 1:1 room with this conf user ID!)
|
|
||||||
const call = jsCreateNewMatrixCall(self.client, room.roomId);
|
|
||||||
call.confUserId = self.confUserId;
|
|
||||||
call.groupRoomId = self.groupRoomId;
|
|
||||||
return call;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ConferenceCall.prototype._joinConferenceUser = function() {
|
|
||||||
// Make sure the conference user is in the group chat room
|
|
||||||
const groupRoom = this.client.getRoom(this.groupRoomId);
|
|
||||||
if (!groupRoom) {
|
|
||||||
return Promise.reject("Bad group room ID");
|
|
||||||
}
|
|
||||||
const member = groupRoom.getMember(this.confUserId);
|
|
||||||
if (member && member.membership === "join") {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return this.client.invite(this.groupRoomId, this.confUserId);
|
|
||||||
};
|
|
||||||
|
|
||||||
ConferenceCall.prototype._getConferenceUserRoom = function() {
|
|
||||||
// Use an existing 1:1 with the conference user; else make one
|
|
||||||
const rooms = this.client.getRooms();
|
|
||||||
let confRoom = null;
|
|
||||||
for (let i = 0; i < rooms.length; i++) {
|
|
||||||
const confUser = rooms[i].getMember(this.confUserId);
|
|
||||||
if (confUser && confUser.membership === "join" &&
|
|
||||||
rooms[i].getJoinedMemberCount() === 2) {
|
|
||||||
confRoom = rooms[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (confRoom) {
|
|
||||||
return Promise.resolve(confRoom);
|
|
||||||
}
|
|
||||||
return this.client.createRoom({
|
|
||||||
preset: "private_chat",
|
|
||||||
invite: [this.confUserId],
|
|
||||||
}).then(function(res) {
|
|
||||||
return new Room(res.room_id, null, MatrixClientPeg.get().getUserId());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this user ID is in fact a conference bot.
|
|
||||||
* @param {string} userId The user ID to check.
|
|
||||||
* @return {boolean} True if it is a conference bot.
|
|
||||||
*/
|
|
||||||
export function isConferenceUser(userId) {
|
|
||||||
if (userId.indexOf("@" + USER_PREFIX) !== 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const base64part = userId.split(":")[0].substring(1 + USER_PREFIX.length);
|
|
||||||
if (base64part) {
|
|
||||||
const decoded = new Buffer(base64part, "base64").toString();
|
|
||||||
// ! $STUFF : $STUFF
|
|
||||||
return /^!.+:.+/.test(decoded);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConferenceUserIdForRoom(roomId) {
|
|
||||||
// abuse browserify's core node Buffer support (strip padding ='s)
|
|
||||||
const base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, "");
|
|
||||||
return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createNewMatrixCall(client, roomId) {
|
|
||||||
const confCall = new ConferenceCall(
|
|
||||||
client, roomId,
|
|
||||||
);
|
|
||||||
return confCall.setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConferenceCallForRoom(roomId) {
|
|
||||||
// search for a conference 1:1 call for this group chat room ID
|
|
||||||
const activeCall = CallHandler.getAnyActiveCall();
|
|
||||||
if (activeCall && activeCall.confUserId) {
|
|
||||||
const thisRoomConfUserId = getConferenceUserIdForRoom(
|
|
||||||
roomId,
|
|
||||||
);
|
|
||||||
if (thisRoomConfUserId === activeCall.confUserId) {
|
|
||||||
return activeCall;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Document this.
|
|
||||||
export const slot = 'conference';
|
|
|
@ -85,7 +85,6 @@ interface IProps {
|
||||||
threepidInvite?: IThreepidInvite;
|
threepidInvite?: IThreepidInvite;
|
||||||
roomOobData?: object;
|
roomOobData?: object;
|
||||||
currentRoomId: string;
|
currentRoomId: string;
|
||||||
ConferenceHandler?: object;
|
|
||||||
collapseLhs: boolean;
|
collapseLhs: boolean;
|
||||||
config: {
|
config: {
|
||||||
piwik: {
|
piwik: {
|
||||||
|
@ -637,7 +636,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
viaServers={this.props.viaServers}
|
viaServers={this.props.viaServers}
|
||||||
key={this.props.currentRoomId || 'roomview'}
|
key={this.props.currentRoomId || 'roomview'}
|
||||||
disabled={this.props.middleDisabled}
|
disabled={this.props.middleDisabled}
|
||||||
ConferenceHandler={this.props.ConferenceHandler}
|
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -149,7 +149,6 @@ interface IRoomInfo {
|
||||||
interface IProps { // TODO type things better
|
interface IProps { // TODO type things better
|
||||||
config: Record<string, any>;
|
config: Record<string, any>;
|
||||||
serverConfig?: ValidatedServerConfig;
|
serverConfig?: ValidatedServerConfig;
|
||||||
ConferenceHandler?: any;
|
|
||||||
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
||||||
enableGuest?: boolean;
|
enableGuest?: boolean;
|
||||||
// the queryParams extracted from the [real] query-string of the URI
|
// the queryParams extracted from the [real] query-string of the URI
|
||||||
|
|
|
@ -69,7 +69,6 @@ import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import TintableSvg from "../views/elements/TintableSvg";
|
import TintableSvg from "../views/elements/TintableSvg";
|
||||||
import type * as ConferenceHandler from '../../VectorConferenceHandler';
|
|
||||||
import {XOR} from "../../@types/common";
|
import {XOR} from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
|
|
||||||
|
@ -84,8 +83,6 @@ if (DEBUG) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
ConferenceHandler?: ConferenceHandler;
|
|
||||||
|
|
||||||
threepidInvite: IThreepidInvite,
|
threepidInvite: IThreepidInvite,
|
||||||
|
|
||||||
// Any data about the room that would normally come from the homeserver
|
// Any data about the room that would normally come from the homeserver
|
||||||
|
@ -181,7 +178,6 @@ export interface IState {
|
||||||
matrixClientIsReady: boolean;
|
matrixClientIsReady: boolean;
|
||||||
showUrlPreview?: boolean;
|
showUrlPreview?: boolean;
|
||||||
e2eStatus?: E2EStatus;
|
e2eStatus?: E2EStatus;
|
||||||
displayConfCallNotification?: boolean;
|
|
||||||
rejecting?: boolean;
|
rejecting?: boolean;
|
||||||
rejectError?: Error;
|
rejectError?: Error;
|
||||||
}
|
}
|
||||||
|
@ -488,8 +484,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
callState: callState,
|
callState: callState,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateConfCallNotification();
|
|
||||||
|
|
||||||
window.addEventListener('beforeunload', this.onPageUnload);
|
window.addEventListener('beforeunload', this.onPageUnload);
|
||||||
if (this.props.resizeNotifier) {
|
if (this.props.resizeNotifier) {
|
||||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||||
|
@ -724,10 +718,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
callState = call.call_state;
|
callState = call.call_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// possibly remove the conf call notification if we're now in
|
|
||||||
// the conf
|
|
||||||
this.updateConfCallNotification();
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
callState: callState,
|
callState: callState,
|
||||||
});
|
});
|
||||||
|
@ -1018,9 +1008,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// rate limited because a power level change will emit an event for every member in the room.
|
// rate limited because a power level change will emit an event for every member in the room.
|
||||||
private updateRoomMembers = rateLimitedFunc((dueToMember) => {
|
private updateRoomMembers = rateLimitedFunc((dueToMember) => {
|
||||||
// a member state changed in this room
|
|
||||||
// refresh the conf call notification state
|
|
||||||
this.updateConfCallNotification();
|
|
||||||
this.updateDMState();
|
this.updateDMState();
|
||||||
|
|
||||||
let memberCountInfluence = 0;
|
let memberCountInfluence = 0;
|
||||||
|
@ -1049,30 +1036,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
|
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateConfCallNotification() {
|
|
||||||
const room = this.state.room;
|
|
||||||
if (!room || !this.props.ConferenceHandler) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const confMember = room.getMember(
|
|
||||||
this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confMember) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const confCall = this.props.ConferenceHandler.getConferenceCallForRoom(confMember.roomId);
|
|
||||||
|
|
||||||
// A conf call notification should be displayed if there is an ongoing
|
|
||||||
// conf call but this cilent isn't a part of it.
|
|
||||||
this.setState({
|
|
||||||
displayConfCallNotification: (
|
|
||||||
(!confCall || confCall.call_state === "ended") &&
|
|
||||||
confMember.membership === "join"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateDMState() {
|
private updateDMState() {
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (room.getMyMembership() != "join") {
|
if (room.getMyMembership() != "join") {
|
||||||
|
@ -1681,7 +1644,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return CallHandler.getCallForRoom(this.state.room.roomId);
|
return CallHandler.sharedInstance().getCallForRoom(this.state.room.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this has to be a proper method rather than an unnamed function,
|
// this has to be a proper method rather than an unnamed function,
|
||||||
|
@ -1925,9 +1888,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
fullHeight={false}
|
fullHeight={false}
|
||||||
userId={this.context.credentials.userId}
|
userId={this.context.credentials.userId}
|
||||||
conferenceHandler={this.props.ConferenceHandler}
|
|
||||||
draggingFile={this.state.draggingFile}
|
draggingFile={this.state.draggingFile}
|
||||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
|
||||||
maxHeight={this.state.auxPanelMaxHeight}
|
maxHeight={this.state.auxPanelMaxHeight}
|
||||||
showApps={this.state.showApps}
|
showApps={this.state.showApps}
|
||||||
hideAppsDrawer={false}
|
hideAppsDrawer={false}
|
||||||
|
|
|
@ -39,15 +39,9 @@ export default class AuxPanel extends React.Component {
|
||||||
showApps: PropTypes.bool, // Render apps
|
showApps: PropTypes.bool, // Render apps
|
||||||
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
|
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
|
||||||
|
|
||||||
// Conference Handler implementation
|
|
||||||
conferenceHandler: PropTypes.object,
|
|
||||||
|
|
||||||
// set to true to show the file drop target
|
// set to true to show the file drop target
|
||||||
draggingFile: PropTypes.bool,
|
draggingFile: PropTypes.bool,
|
||||||
|
|
||||||
// set to true to show the 'active conf call' banner
|
|
||||||
displayConfCallNotification: PropTypes.bool,
|
|
||||||
|
|
||||||
// maxHeight attribute for the aux panel and the video
|
// maxHeight attribute for the aux panel and the video
|
||||||
// therein
|
// therein
|
||||||
maxHeight: PropTypes.number,
|
maxHeight: PropTypes.number,
|
||||||
|
@ -161,39 +155,9 @@ export default class AuxPanel extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let conferenceCallNotification = null;
|
|
||||||
if (this.props.displayConfCallNotification) {
|
|
||||||
let supportedText = '';
|
|
||||||
let joinNode;
|
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
|
||||||
supportedText = _t(" (unsupported)");
|
|
||||||
} else {
|
|
||||||
joinNode = (<span>
|
|
||||||
{ _t(
|
|
||||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
|
|
||||||
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
|
|
||||||
},
|
|
||||||
) }
|
|
||||||
</span>);
|
|
||||||
}
|
|
||||||
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
|
||||||
// but there are translations for this in the languages we do have so I'm leaving it for now.
|
|
||||||
conferenceCallNotification = (
|
|
||||||
<div className="mx_RoomView_ongoingConfCallNotification">
|
|
||||||
{ _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }
|
|
||||||
|
|
||||||
{ joinNode }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const callView = (
|
const callView = (
|
||||||
<CallView
|
<CallView
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
ConferenceHandler={this.props.conferenceHandler}
|
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
maxVideoHeight={this.props.maxHeight}
|
maxVideoHeight={this.props.maxHeight}
|
||||||
/>
|
/>
|
||||||
|
@ -276,7 +240,6 @@ export default class AuxPanel extends React.Component {
|
||||||
{ appsDrawer }
|
{ appsDrawer }
|
||||||
{ fileDropTarget }
|
{ fileDropTarget }
|
||||||
{ callView }
|
{ callView }
|
||||||
{ conferenceCallNotification }
|
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,7 +24,6 @@ import {isValid3pidInvite} from "../../../RoomInvite";
|
||||||
import rate_limited_func from "../../../ratelimitedfunc";
|
import rate_limited_func from "../../../ratelimitedfunc";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import CallHandler from "../../../CallHandler";
|
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||||
import BaseCard from "../right_panel/BaseCard";
|
import BaseCard from "../right_panel/BaseCard";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||||
|
@ -233,15 +232,10 @@ export default class MemberList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
roomMembers() {
|
roomMembers() {
|
||||||
const ConferenceHandler = CallHandler.getConferenceHandler();
|
|
||||||
|
|
||||||
const allMembers = this.getMembersWithUser();
|
const allMembers = this.getMembersWithUser();
|
||||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||||
return (
|
return (
|
||||||
m.membership === 'join' || m.membership === 'invite'
|
m.membership === 'join' || m.membership === 'invite'
|
||||||
) && (
|
|
||||||
!ConferenceHandler ||
|
|
||||||
(ConferenceHandler && !ConferenceHandler.isConferenceUser(m.userId))
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
filteredAndSortedMembers.sort(this.memberSort);
|
filteredAndSortedMembers.sort(this.memberSort);
|
||||||
|
|
|
@ -87,7 +87,7 @@ VideoCallButton.propTypes = {
|
||||||
function HangupButton(props) {
|
function HangupButton(props) {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const onHangupClick = () => {
|
const onHangupClick = () => {
|
||||||
const call = CallHandler.getCallForRoom(props.roomId);
|
const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
|
||||||
if (!call) {
|
if (!call) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import IncomingCallBox from './IncomingCallBox';
|
import IncomingCallBox from './IncomingCallBox';
|
||||||
import CallPreview from './CallPreview';
|
import CallPreview from './CallPreview';
|
||||||
import * as VectorConferenceHandler from '../../../VectorConferenceHandler';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
||||||
|
@ -31,7 +30,7 @@ export default class CallContainer extends React.PureComponent<IProps, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
return <div className="mx_CallContainer">
|
return <div className="mx_CallContainer">
|
||||||
<IncomingCallBox />
|
<IncomingCallBox />
|
||||||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
<CallPreview />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,6 @@ import PersistentApp from "../elements/PersistentApp";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// A Conference Handler implementation
|
|
||||||
// Must have a function signature:
|
|
||||||
// getConferenceCallForRoom(roomId: string): MatrixCall
|
|
||||||
ConferenceHandler: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -47,7 +43,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
activeCall: CallHandler.getAnyActiveCall(),
|
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,14 +73,14 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
// 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':
|
||||||
this.setState({
|
this.setState({
|
||||||
activeCall: CallHandler.getAnyActiveCall(),
|
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCallViewClick = () => {
|
private onCallViewClick = () => {
|
||||||
const call = CallHandler.getAnyActiveCall();
|
const call = CallHandler.sharedInstance().getAnyActiveCall();
|
||||||
if (call) {
|
if (call) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
|
@ -94,7 +90,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
|
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
|
||||||
const showCall = (
|
const showCall = (
|
||||||
this.state.activeCall &&
|
this.state.activeCall &&
|
||||||
this.state.activeCall.call_state === 'connected' &&
|
this.state.activeCall.call_state === 'connected' &&
|
||||||
|
@ -106,7 +102,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
<CallView
|
<CallView
|
||||||
className="mx_CallPreview"
|
className="mx_CallPreview"
|
||||||
onClick={this.onCallViewClick}
|
onClick={this.onCallViewClick}
|
||||||
ConferenceHandler={this.props.ConferenceHandler}
|
|
||||||
showHangup={true}
|
showHangup={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,11 +31,6 @@ interface IProps {
|
||||||
// room; if not, we will show any active call.
|
// room; if not, we will show any active call.
|
||||||
room?: Room;
|
room?: Room;
|
||||||
|
|
||||||
// A Conference Handler implementation
|
|
||||||
// Must have a function signature:
|
|
||||||
// getConferenceCallForRoom(roomId: string): MatrixCall
|
|
||||||
ConferenceHandler?: any;
|
|
||||||
|
|
||||||
// maxHeight style attribute for the video panel
|
// maxHeight style attribute for the video panel
|
||||||
maxVideoHeight?: number;
|
maxVideoHeight?: number;
|
||||||
|
|
||||||
|
@ -96,14 +91,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
call = CallHandler.getCallForRoom(roomId) ||
|
call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||||
(this.props.ConferenceHandler ? this.props.ConferenceHandler.getConferenceCallForRoom(roomId) : null);
|
|
||||||
|
|
||||||
if (this.call) {
|
if (this.call) {
|
||||||
this.setState({ call: call });
|
this.setState({ call: call });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
call = CallHandler.getAnyActiveCall();
|
call = CallHandler.sharedInstance().getAnyActiveCall();
|
||||||
// Ignore calls if we can't get the room associated with them.
|
// Ignore calls if we can't get the room associated with them.
|
||||||
// I think the underlying problem is that the js-sdk sends events
|
// I think the underlying problem is that the js-sdk sends events
|
||||||
// for calls before it has made the rooms available in the store,
|
// for calls before it has made the rooms available in the store,
|
||||||
|
@ -115,20 +109,19 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (call) {
|
if (call) {
|
||||||
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
if (this.getVideoView()) {
|
||||||
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
||||||
// always use a separate element for audio stream playback.
|
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
||||||
// this is to let us move CallView around the DOM without interrupting remote audio
|
|
||||||
// during playback, by having the audio rendered by a top-level <audio/> element.
|
// always use a separate element for audio stream playback.
|
||||||
// rather than being rendered by the main remoteVideo <video/> element.
|
// this is to let us move CallView around the DOM without interrupting remote audio
|
||||||
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
// during playback, by having the audio rendered by a top-level <audio/> element.
|
||||||
|
// rather than being rendered by the main remoteVideo <video/> element.
|
||||||
|
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
|
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
|
||||||
// if this call is a conf call, don't display local video as the
|
this.getVideoView().getLocalVideoElement().style.display = "block";
|
||||||
// conference will have us in it
|
|
||||||
this.getVideoView().getLocalVideoElement().style.display = (
|
|
||||||
call.confUserId ? "none" : "block"
|
|
||||||
);
|
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
this.getVideoView().getLocalVideoElement().style.display = "none";
|
this.getVideoView().getLocalVideoElement().style.display = "none";
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'call_state': {
|
case 'call_state': {
|
||||||
const call = CallHandler.getCall(payload.room_id);
|
const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
|
||||||
if (call && call.call_state === 'ringing') {
|
if (call && call.call_state === 'ringing') {
|
||||||
this.setState({
|
this.setState({
|
||||||
incomingCall: call,
|
incomingCall: call,
|
||||||
|
|
|
@ -214,7 +214,6 @@
|
||||||
"Reason": "Reason",
|
"Reason": "Reason",
|
||||||
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
|
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
|
||||||
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
|
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
|
||||||
"%(senderName)s requested a VoIP conference.": "%(senderName)s requested a VoIP conference.",
|
|
||||||
"%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.",
|
"%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.",
|
||||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.",
|
"%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.",
|
||||||
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.",
|
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.",
|
||||||
|
@ -224,9 +223,7 @@
|
||||||
"%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
|
"%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
|
||||||
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
|
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
|
||||||
"%(senderName)s made no change.": "%(senderName)s made no change.",
|
"%(senderName)s made no change.": "%(senderName)s made no change.",
|
||||||
"VoIP conference started.": "VoIP conference started.",
|
|
||||||
"%(targetName)s joined the room.": "%(targetName)s joined the room.",
|
"%(targetName)s joined the room.": "%(targetName)s joined the room.",
|
||||||
"VoIP conference finished.": "VoIP conference finished.",
|
|
||||||
"%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.",
|
"%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.",
|
||||||
"%(targetName)s left the room.": "%(targetName)s left the room.",
|
"%(targetName)s left the room.": "%(targetName)s left the room.",
|
||||||
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.",
|
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.",
|
||||||
|
@ -1033,9 +1030,6 @@
|
||||||
"Add a widget": "Add a widget",
|
"Add a widget": "Add a widget",
|
||||||
"Drop File Here": "Drop File Here",
|
"Drop File Here": "Drop File Here",
|
||||||
"Drop file here to upload": "Drop file here to upload",
|
"Drop file here to upload": "Drop file here to upload",
|
||||||
" (unsupported)": " (unsupported)",
|
|
||||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
|
||||||
"Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.",
|
|
||||||
"This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
|
"This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
|
||||||
"You have not verified this user.": "You have not verified this user.",
|
"You have not verified this user.": "You have not verified this user.",
|
||||||
"You have verified this user. This user has verified all of their sessions.": "You have verified this user. This user has verified all of their sessions.",
|
"You have verified this user. This user has verified all of their sessions.": "You have verified this user. This user has verified all of their sessions.",
|
||||||
|
|
Loading…
Reference in a new issue