Make CallHandler more EventEmittery (#6704)

* sharedInstance() -> instance

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use CallState event instead of dispatching

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Simplifie some code

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use a method to start a call instead of the dispatcher

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use a method instead of place_conference_call

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make terminateCallApp() and hangupCallApp() public

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use hangupAllCalls() instead of the dispatcher

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make dialNumber(), startTransferToMatrixID() and startTransferToPhoneNumber() public instead of using the dispatcher

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use answerCall() instead of using the dispatcher

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use hangupOrReject() instead of the dispatcher

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Update docs

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Dispatch call_state, see https://github.com/vector-im/element-web/pull/18823#issuecomment-917377277

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add missing import

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-11-30 19:09:13 +01:00 committed by GitHub
parent e3187ed15c
commit cbb34d8ac7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 300 additions and 511 deletions

View file

@ -17,43 +17,6 @@ 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';
@ -65,7 +28,6 @@ import SettingsStore from './settings/SettingsStore';
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";
@ -133,24 +95,24 @@ interface ThirdpartyLookupResponse {
fields: ThirdpartyLookupResponseFields;
}
export enum PlaceCallType {
Voice = 'voice',
Video = 'video',
}
export enum CallHandlerEvent {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
CallState = "call_state",
}
/**
* CallHandler manages all currently active calls. It should be used for
* placing, answering, rejecting and hanging up calls. It also handles ringing,
* PSTN support and other things.
*/
export default class CallHandler extends EventEmitter {
private calls = new Map<string, MatrixCall>(); // roomId -> call
// Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one.
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
private audioPromises = new Map<AudioID, Promise<void>>();
private dispatcherRef: string = null;
private supportsPstnProtocol = null;
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
@ -166,7 +128,7 @@ export default class CallHandler extends EventEmitter {
private silencedCalls = new Set<string>(); // callIds
static sharedInstance() {
public static get instance() {
if (!window.mxCallHandler) {
window.mxCallHandler = new CallHandler();
}
@ -194,8 +156,7 @@ export default class CallHandler extends EventEmitter {
return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId;
}
start() {
this.dispatcherRef = dis.register(this.onAction);
public start(): void {
// 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.
@ -215,18 +176,14 @@ export default class CallHandler extends EventEmitter {
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
}
stop() {
public stop(): void {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener('Call.incoming', this.onCallIncoming);
}
if (this.dispatcherRef !== null) {
dis.unregister(this.dispatcherRef);
this.dispatcherRef = null;
}
}
public silenceCall(callId: string) {
public silenceCall(callId: string): void {
this.silencedCalls.add(callId);
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
@ -235,7 +192,7 @@ export default class CallHandler extends EventEmitter {
this.pause(AudioID.Ring);
}
public unSilenceCall(callId: string) {
public unSilenceCall(callId: string): void {
this.silencedCalls.delete(callId);
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
this.play(AudioID.Ring);
@ -261,7 +218,7 @@ export default class CallHandler extends EventEmitter {
return false;
}
private async checkProtocols(maxTries) {
private async checkProtocols(maxTries: number): Promise<void> {
try {
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
@ -296,11 +253,11 @@ export default class CallHandler extends EventEmitter {
}
}
public getSupportsPstnProtocol() {
public getSupportsPstnProtocol(): boolean {
return this.supportsPstnProtocol;
}
public getSupportsVirtualRooms() {
public getSupportsVirtualRooms(): boolean {
return this.supportsSipNativeVirtual;
}
@ -328,14 +285,32 @@ export default class CallHandler extends EventEmitter {
);
}
private onCallIncoming = (call) => {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
private onCallIncoming = (call: MatrixCall): void => {
// if the runtime env doesn't do VoIP, stop here.
if (!MatrixClientPeg.get().supportsVoip()) {
return;
}
const mappedRoomId = CallHandler.instance.roomIdForCall(call);
if (this.getCallForRoom(mappedRoomId)) {
logger.log(
"Got incoming call for room " + mappedRoomId +
" but there's already a call for this room: ignoring",
);
return;
}
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
this.addCallForRoom(mappedRoomId, call);
this.setCallListeners(call);
// Explicitly handle first state change
this.onCallStateChanged(call.state, null, call);
// get ready to send encrypted events in the room, so if the user does answer
// the call, we'll be ready to send. NB. This is the protocol-level room ID not
// the mapped one: that's where we'll send the events.
const cli = MatrixClientPeg.get();
cli.prepareToEncrypt(cli.getRoom(call.roomId));
};
public getCallById(callId: string): MatrixCall {
@ -345,11 +320,11 @@ export default class CallHandler extends EventEmitter {
return null;
}
getCallForRoom(roomId: string): MatrixCall {
public getCallForRoom(roomId: string): MatrixCall | null {
return this.calls.get(roomId) || null;
}
getAnyActiveCall() {
public getAnyActiveCall(): MatrixCall | null {
for (const call of this.calls.values()) {
if (call.state !== CallState.Ended) {
return call;
@ -358,7 +333,7 @@ export default class CallHandler extends EventEmitter {
return null;
}
getAllActiveCalls() {
public getAllActiveCalls(): MatrixCall[] {
const activeCalls = [];
for (const call of this.calls.values()) {
@ -369,7 +344,7 @@ export default class CallHandler extends EventEmitter {
return activeCalls;
}
getAllActiveCallsNotInRoom(notInThisRoomId) {
public getAllActiveCallsNotInRoom(notInThisRoomId: string): MatrixCall[] {
const callsNotInThatRoom = [];
for (const [roomId, call] of this.calls.entries()) {
@ -390,11 +365,11 @@ export default class CallHandler extends EventEmitter {
return this.getAllActiveCallsNotInRoom(roomId);
}
getTransfereeForCallId(callId: string): MatrixCall {
public getTransfereeForCallId(callId: string): MatrixCall {
return this.transferees[callId];
}
play(audioId: AudioID) {
public play(audioId: AudioID): void {
// TODO: Attach an invisible element for this instead
// which listens?
const audio = document.getElementById(audioId) as HTMLMediaElement;
@ -423,7 +398,7 @@ export default class CallHandler extends EventEmitter {
}
}
pause(audioId: AudioID) {
public pause(audioId: AudioID): void {
// TODO: Attach an invisible element for this instead
// which listens?
const audio = document.getElementById(audioId) as HTMLMediaElement;
@ -437,7 +412,7 @@ export default class CallHandler extends EventEmitter {
}
}
private matchesCallForThisRoom(call: MatrixCall) {
private matchesCallForThisRoom(call: MatrixCall): boolean {
// We don't allow placing more than one call per room, but that doesn't mean there
// can't be more than one, eg. in a glare situation. This checks that the given call
// is the call we consider 'the' call for its room.
@ -447,7 +422,7 @@ export default class CallHandler extends EventEmitter {
return callForThisRoom && call.callId === callForThisRoom.callId;
}
private setCallListeners(call: MatrixCall) {
private setCallListeners(call: MatrixCall): void {
let mappedRoomId = this.roomIdForCall(call);
call.on(CallEvent.Error, (err: CallError) => {
@ -542,6 +517,11 @@ export default class CallHandler extends EventEmitter {
const mappedRoomId = this.roomIdForCall(call);
this.setCallState(call, newState);
dis.dispatch({
action: 'call_state',
room_id: mappedRoomId,
state: newState,
});
switch (oldState) {
case CallState.Ringing:
@ -620,7 +600,7 @@ export default class CallHandler extends EventEmitter {
}
};
private async logCallStats(call: MatrixCall, mappedRoomId: string) {
private async logCallStats(call: MatrixCall, mappedRoomId: string): Promise<void> {
const stats = await call.getCurrentCallStats();
logger.debug(
`Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` +
@ -663,8 +643,8 @@ export default class CallHandler extends EventEmitter {
}
}
private setCallState(call: MatrixCall, status: CallState) {
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
private setCallState(call: MatrixCall, status: CallState): void {
const mappedRoomId = CallHandler.instance.roomIdForCall(call);
logger.log(
`Call state in ${mappedRoomId} changed to ${status}`,
@ -683,20 +663,16 @@ export default class CallHandler extends EventEmitter {
ToastStore.sharedInstance().dismissToast(toastKey);
}
dis.dispatch({
action: 'call_state',
room_id: mappedRoomId,
state: status,
});
this.emit(CallHandlerEvent.CallState, mappedRoomId, status);
}
private removeCallForRoom(roomId: string) {
private removeCallForRoom(roomId: string): void {
logger.log("Removing call for room ", roomId);
this.calls.delete(roomId);
this.emit(CallHandlerEvent.CallsChanged, this.calls);
}
private showICEFallbackPrompt() {
private showICEFallbackPrompt(): void {
const cli = MatrixClientPeg.get();
const code = sub => <code>{ sub }</code>;
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
@ -725,7 +701,7 @@ export default class CallHandler extends EventEmitter {
}, null, true);
}
private showMediaCaptureError(call: MatrixCall) {
private showMediaCaptureError(call: MatrixCall): void {
let title;
let description;
@ -754,9 +730,9 @@ export default class CallHandler extends EventEmitter {
}, null, true);
}
private async placeCall(roomId: string, type: PlaceCallType, transferee: MatrixCall) {
private async placeMatrixCall(roomId: string, type: CallType, transferee?: MatrixCall): Promise<void> {
Analytics.trackEvent('voip', 'placeCall', 'type', type);
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
CountlyAnalytics.instance.trackStartCall(roomId, type === CallType.Video, false);
const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId;
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
@ -782,7 +758,7 @@ export default class CallHandler extends EventEmitter {
this.setActiveCallRoomId(roomId);
if (type === PlaceCallType.Voice) {
if (type === CallType.Voice) {
call.placeVoiceCall();
} else if (type === 'video') {
call.placeVideoCall();
@ -791,166 +767,102 @@ export default class CallHandler extends EventEmitter {
}
}
private onAction = (payload: ActionPayload) => {
switch (payload.action) {
case 'place_call':
{
// We might be using managed hybrid widgets
if (isManagedHybridWidgetEnabled()) {
addManagedHybridWidget(payload.room_id);
return;
}
// 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;
}
// 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);
if (!room) {
logger.error(`Room ${payload.room_id} does not exist.`);
return;
}
// We leave the check for whether there's already a call in this room until later,
// otherwise it can race.
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) {
logger.info(`Place ${payload.type} call in ${payload.room_id}`);
this.placeCall(payload.room_id, payload.type, payload.transferee);
} else { // > 2
dis.dispatch({
action: "place_conference_call",
room_id: payload.room_id,
type: payload.type,
});
}
}
break;
case 'place_conference_call':
logger.info("Place conference call in " + payload.room_id);
Analytics.trackEvent('voip', 'placeConferenceCall');
CountlyAnalytics.instance.trackStartCall(payload.room_id, payload.type === PlaceCallType.Video, true);
this.startCallApp(payload.room_id, payload.type);
break;
case 'end_conference':
logger.info("Terminating conference call in " + payload.room_id);
this.terminateCallApp(payload.room_id);
break;
case 'hangup_conference':
logger.info("Leaving conference call in "+ payload.room_id);
this.hangupCallApp(payload.room_id);
break;
case 'incoming_call':
{
// if the runtime env doesn't do VoIP, stop here.
if (!MatrixClientPeg.get().supportsVoip()) {
return;
}
const call = payload.call as MatrixCall;
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
if (this.getCallForRoom(mappedRoomId)) {
logger.log(
"Got incoming call for room " + mappedRoomId +
" but there's already a call for this room: ignoring",
);
return;
}
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
this.addCallForRoom(mappedRoomId, call);
this.setCallListeners(call);
// Explicitly handle first state change
this.onCallStateChanged(call.state, null, call);
// get ready to send encrypted events in the room, so if the user does answer
// the call, we'll be ready to send. NB. This is the protocol-level room ID not
// the mapped one: that's where we'll send the events.
const cli = MatrixClientPeg.get();
cli.prepareToEncrypt(cli.getRoom(call.roomId));
}
break;
case 'hangup':
case 'reject':
this.stopRingingIfPossible(this.calls.get(payload.room_id).callId);
if (!this.calls.get(payload.room_id)) {
return; // no call to hangup
}
if (payload.action === 'reject') {
this.calls.get(payload.room_id).reject();
} else {
this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false);
}
// don't remove the call yet: let the hangup event handler do it (otherwise it will throw
// the hangup event away)
break;
case 'hangup_all':
this.stopRingingIfPossible(this.calls.get(payload.room_id).callId);
for (const call of this.calls.values()) {
call.hangup(CallErrorCode.UserHangup, false);
}
break;
case 'answer': {
this.stopRingingIfPossible(this.calls.get(payload.room_id).callId);
if (!this.calls.has(payload.room_id)) {
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);
call.answer();
this.setActiveCallRoomId(payload.room_id);
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
dis.dispatch({
action: Action.ViewRoom,
room_id: payload.room_id,
});
break;
}
case Action.DialNumber:
this.dialNumber(payload.number);
break;
case Action.TransferCallToMatrixID:
this.startTransferToMatrixID(payload.call, payload.destination, payload.consultFirst);
break;
case Action.TransferCallToPhoneNumber:
this.startTransferToPhoneNumber(payload.call, payload.destination, payload.consultFirst);
break;
public placeCall(roomId: string, type?: CallType, transferee?: MatrixCall): void {
// We might be using managed hybrid widgets
if (isManagedHybridWidgetEnabled()) {
addManagedHybridWidget(roomId);
return;
}
};
// 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;
}
// 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(roomId);
if (!room) {
logger.error(`Room ${roomId} does not exist.`);
return;
}
// We leave the check for whether there's already a call in this room until later,
// otherwise it can race.
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.'),
});
} else if (members.length === 2) {
logger.info(`Place ${type} call in ${roomId}`);
this.placeMatrixCall(roomId, type, transferee);
} else { // > 2
this.placeJitsiCall(roomId, type);
}
}
public hangupAllCalls(): void {
for (const call of this.calls.values()) {
this.stopRingingIfPossible(call.callId);
call.hangup(CallErrorCode.UserHangup, false);
}
}
public hangupOrReject(roomId: string, reject?: boolean): void {
const call = this.calls.get(roomId);
// no call to hangup
if (!call) return;
this.stopRingingIfPossible(call.callId);
if (reject) {
call.reject();
} else {
call.hangup(CallErrorCode.UserHangup, false);
}
// don't remove the call yet: let the hangup event handler do it (otherwise it will throw
// the hangup event away)
}
public answerCall(roomId: string): void {
const call = this.calls.get(roomId);
this.stopRingingIfPossible(call.callId);
// no call to answer
if (!this.calls.has(roomId)) return;
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;
}
call.answer();
this.setActiveCallRoomId(roomId);
CountlyAnalytics.instance.trackJoinCall(roomId, call.type === CallType.Video, false);
dis.dispatch({
action: Action.ViewRoom,
room_id: roomId,
});
}
private stopRingingIfPossible(callId: string): void {
this.silencedCalls.delete(callId);
@ -958,7 +870,7 @@ export default class CallHandler extends EventEmitter {
this.pause(AudioID.Ring);
}
private async dialNumber(number: string) {
public async dialNumber(number: string): Promise<void> {
const results = await this.pstnLookup(number);
if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, {
@ -988,10 +900,12 @@ export default class CallHandler extends EventEmitter {
room_id: roomId,
});
await this.placeCall(roomId, PlaceCallType.Voice, null);
await this.placeMatrixCall(roomId, CallType.Voice, null);
}
private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) {
public async startTransferToPhoneNumber(
call: MatrixCall, destination: string, consultFirst: boolean,
): Promise<void> {
const results = await this.pstnLookup(destination);
if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, {
@ -1004,16 +918,13 @@ export default class CallHandler extends EventEmitter {
await this.startTransferToMatrixID(call, results[0].userid, consultFirst);
}
private async startTransferToMatrixID(call: MatrixCall, destination: string, consultFirst: boolean) {
public async startTransferToMatrixID(
call: MatrixCall, destination: string, consultFirst: boolean,
): Promise<void> {
if (consultFirst) {
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination);
dis.dispatch({
action: 'place_call',
type: call.type,
room_id: dmRoomId,
transferee: call,
});
this.placeCall(dmRoomId, call.type, call);
dis.dispatch({
action: Action.ViewRoom,
room_id: dmRoomId,
@ -1033,7 +944,7 @@ export default class CallHandler extends EventEmitter {
}
}
setActiveCallRoomId(activeCallRoomId: string) {
public setActiveCallRoomId(activeCallRoomId: string): void {
logger.info("Setting call in room " + activeCallRoomId + " active");
for (const [roomId, call] of this.calls.entries()) {
@ -1051,7 +962,7 @@ export default class CallHandler extends EventEmitter {
/**
* @returns true if we are currently in any call where we haven't put the remote party on hold
*/
hasAnyUnheldCall() {
public hasAnyUnheldCall(): boolean {
for (const call of this.calls.values()) {
if (call.state === CallState.Ended) continue;
if (!call.isRemoteOnHold()) return true;
@ -1060,7 +971,11 @@ export default class CallHandler extends EventEmitter {
return false;
}
private async startCallApp(roomId: string, type: string) {
private async placeJitsiCall(roomId: string, type: string): Promise<void> {
logger.info("Place conference call in " + roomId);
Analytics.trackEvent('voip', 'placeConferenceCall');
CountlyAnalytics.instance.trackStartCall(roomId, type === CallType.Video, true);
dis.dispatch({
action: 'appsDrawer',
show: true,
@ -1126,7 +1041,9 @@ export default class CallHandler extends EventEmitter {
});
}
private terminateCallApp(roomId: string) {
public terminateCallApp(roomId: string): void {
logger.info("Terminating conference call in " + roomId);
Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
hasCancelButton: true,
title: _t("End conference"),
@ -1147,7 +1064,9 @@ export default class CallHandler extends EventEmitter {
});
}
private hangupCallApp(roomId: string) {
public hangupCallApp(roomId: string): void {
logger.info("Leaving conference call in " + roomId);
const roomInfo = WidgetStore.instance.getRoom(roomId);
if (!roomInfo) return; // "should never happen" clauses go here

View file

@ -794,7 +794,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.instance.start();
CallHandler.sharedInstance().start();
CallHandler.instance.start();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
// the thing just wastes CPU cycles, but should result in no actual functionality
@ -897,7 +897,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
*/
export function stopMatrixClient(unsetClient = true): void {
Notifier.stop();
CallHandler.sharedInstance().stop();
CallHandler.instance.stop();
UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset();
Presence.stop();

View file

@ -1037,7 +1037,7 @@ export const Commands = [
return success((async () => {
if (isPhoneNumber) {
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
const results = await CallHandler.instance.pstnLookup(this.state.value);
if (!results || results.length === 0 || !results[0].userid) {
throw new Error("Unable to find Matrix ID for phone number");
}
@ -1089,7 +1089,7 @@ export const Commands = [
description: _td("Places the call in the current room on hold"),
category: CommandCategories.other,
runFn: function(roomId, args) {
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
const call = CallHandler.instance.getCallForRoom(roomId);
if (!call) {
return reject("No active call in this room");
}
@ -1103,7 +1103,7 @@ export const Commands = [
description: _td("Takes the call in the current room off hold"),
category: CommandCategories.other,
runFn: function(roomId, args) {
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
const call = CallHandler.instance.getCallForRoom(roomId);
if (!call) {
return reject("No active call in this room");
}

View file

@ -36,7 +36,7 @@ export default class VoipUserMapper {
}
private async userToVirtualUser(userId: string): Promise<string> {
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
const results = await CallHandler.instance.sipVirtualLookup(userId);
if (results.length === 0 || !results[0].fields.lookup_success) return null;
return results[0].userid;
}
@ -97,11 +97,11 @@ export default class VoipUserMapper {
}
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
if (!CallHandler.instance.getSupportsVirtualRooms()) return;
const inviterId = invitedRoom.getDMInviter();
logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
const result = await CallHandler.instance.sipNativeLookup(inviterId);
if (result.length === 0) {
return;
}

View file

@ -20,7 +20,6 @@ import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/we
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { EventEmitter } from 'events';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
export enum CallEventGrouperEvent {
StateChanged = "state_changed",
@ -52,8 +51,8 @@ export default class CallEventGrouper extends EventEmitter {
constructor() {
super();
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall);
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
CallHandler.instance.addListener(CallHandlerEvent.CallsChanged, this.setCall);
CallHandler.instance.addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
}
private get invite(): MatrixEvent {
@ -114,7 +113,7 @@ export default class CallEventGrouper extends EventEmitter {
}
private onSilencedCallsChanged = () => {
const newState = CallHandler.sharedInstance().isCallSilenced(this.callId);
const newState = CallHandler.instance.isCallSilenced(this.callId);
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
};
@ -122,33 +121,23 @@ export default class CallEventGrouper extends EventEmitter {
this.emit(CallEventGrouperEvent.LengthChanged, length);
};
public answerCall = () => {
defaultDispatcher.dispatch({
action: 'answer',
room_id: this.roomId,
});
public answerCall = (): void => {
CallHandler.instance.answerCall(this.roomId);
};
public rejectCall = () => {
defaultDispatcher.dispatch({
action: 'reject',
room_id: this.roomId,
});
public rejectCall = (): void => {
CallHandler.instance.hangupOrReject(this.roomId, true);
};
public callBack = () => {
defaultDispatcher.dispatch({
action: 'place_call',
type: this.isVoice ? CallType.Voice : CallType.Video,
room_id: this.roomId,
});
public callBack = (): void => {
CallHandler.instance.placeCall(this.roomId, this.isVoice ? CallType.Voice : CallType.Video);
};
public toggleSilenced = () => {
const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId);
const silenced = CallHandler.instance.isCallSilenced(this.callId);
silenced ?
CallHandler.sharedInstance().unSilenceCall(this.callId) :
CallHandler.sharedInstance().silenceCall(this.callId);
CallHandler.instance.unSilenceCall(this.callId) :
CallHandler.instance.silenceCall(this.callId);
};
private setCallListeners() {
@ -174,7 +163,7 @@ export default class CallEventGrouper extends EventEmitter {
private setCall = () => {
if (this.call) return;
this.call = CallHandler.sharedInstance().getCallById(this.callId);
this.call = CallHandler.instance.getCallById(this.callId);
this.setCallListeners();
this.setState();
};

View file

@ -340,7 +340,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
// If we have dialer support, show a button to bring up the dial pad
// to start a new call
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
if (CallHandler.instance.getSupportsPstnProtocol()) {
dialPadButton =
<AccessibleTooltipButton
className={classNames("mx_LeftPanel_dialPadButton", {})}

View file

@ -53,7 +53,7 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import { replaceableComponent } from "../../utils/replaceableComponent";
import CallHandler from '../../CallHandler';
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
import { OwnProfileStore } from '../../stores/OwnProfileStore';
@ -164,7 +164,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
usageLimitDismissed: false,
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
activeCalls: CallHandler.instance.getAllActiveCalls(),
};
// stash the MatrixClient in case we log out before we are unmounted
@ -181,7 +181,7 @@ class LoggedInView extends React.Component<IProps, IState> {
componentDidMount() {
document.addEventListener('keydown', this.onNativeKeyDown, false);
this.dispatcherRef = dis.register(this.onAction);
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.onCallState);
this.updateServerNoticeEvents();
@ -212,6 +212,7 @@ class LoggedInView extends React.Component<IProps, IState> {
componentWillUnmount() {
document.removeEventListener('keydown', this.onNativeKeyDown, false);
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.onCallState);
dis.unregister(this.dispatcherRef);
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
@ -222,6 +223,12 @@ class LoggedInView extends React.Component<IProps, IState> {
this.resizer.detach();
}
private onCallState = (): void => {
const activeCalls = CallHandler.instance.getAllActiveCalls();
if (activeCalls === this.state.activeCalls) return;
this.setState({ activeCalls });
};
private refreshBackgroundImage = async (): Promise<void> => {
let backgroundImage = SettingsStore.getValue("RoomList.backgroundImage");
if (backgroundImage) {
@ -233,18 +240,6 @@ class LoggedInView extends React.Component<IProps, IState> {
this.setState({ backgroundImage });
};
private onAction = (payload): void => {
switch (payload.action) {
case 'call_state': {
const activeCalls = CallHandler.sharedInstance().getAllActiveCalls();
if (activeCalls !== this.state.activeCalls) {
this.setState({ activeCalls });
}
break;
}
}
};
public canResetTimelineInRoom = (roomId: string) => {
if (!this._roomView.current) {
return true;

View file

@ -108,6 +108,7 @@ import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
import { copyPlaintext } from "../../utils/strings";
import { PosthogAnalytics } from '../../PosthogAnalytics';
import { initSentry } from "../../sentry";
import CallHandler from "../../CallHandler";
import { logger } from "matrix-js-sdk/src/logger";
import { showSpaceInvite } from "../../utils/space";
@ -604,7 +605,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
break;
case 'logout':
dis.dispatch({ action: "hangup_all" });
CallHandler.instance.hangupAllCalls();
Lifecycle.logout();
break;
case 'require_registration':

View file

@ -34,7 +34,7 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import CallHandler, { PlaceCallType } from '../../CallHandler';
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher';
import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
@ -66,7 +66,7 @@ import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay";
import { containsEmoji } from '../../effects/utils';
import { CHAT_EFFECTS } from '../../effects';
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import WidgetStore from "../../stores/WidgetStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import Notifier from "../../Notifier";
@ -669,6 +669,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
componentDidUpdate() {
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.onCallState);
if (this.roomView.current) {
const roomView = this.roomView.current;
if (!roomView.ondrop) {
@ -699,6 +700,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.onCallState);
// update the scroll map before we get unmounted
if (this.state.roomId) {
RoomScrollStateStore.setScrollState(this.state.roomId, this.getScrollState());
@ -822,6 +825,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
};
private onCallState = (roomId: string): void => {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (!roomId) return;
const call = this.getCallForRoom();
this.setState({ callState: call ? call.state : null });
};
private onAction = payload => {
switch (payload.action) {
case 'message_sent':
@ -843,21 +855,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
case Action.UploadCanceled:
this.forceUpdate();
break;
case 'call_state': {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (!payload.room_id) {
return;
}
const call = this.getCallForRoom();
this.setState({
callState: call ? call.state : null,
});
break;
}
case 'appsDrawer':
this.setState({
showApps: payload.show,
@ -1531,12 +1528,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return ret;
}
private onCallPlaced = (type: PlaceCallType) => {
dis.dispatch({
action: 'place_call',
type: type,
room_id: this.state.room.roomId,
});
private onCallPlaced = (type: CallType): void => {
CallHandler.instance.placeCall(this.state.room?.roomId, type);
};
private onAppsClick = () => {
@ -1748,7 +1741,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room) {
return null;
}
return CallHandler.sharedInstance().getCallForRoom(this.state.room.roomId);
return CallHandler.instance.getCallForRoom(this.state.room.roomId);
}
// this has to be a proper method rather than an unnamed function,

View file

@ -45,7 +45,7 @@ export default class CallContextMenu extends React.Component<IProps> {
};
onUnholdClick = () => {
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
CallHandler.instance.setActiveCallRoomId(this.props.call.roomId);
this.props.onFinished();
};

View file

@ -63,7 +63,6 @@ import { copyPlaintext, selectText } from "../../../utils/strings";
import * as ContextMenu from "../../structures/ContextMenu";
import { toRightOf } from "../../structures/ContextMenu";
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
import { TransferCallPayload } from '../../../dispatcher/payloads/TransferCallPayload';
import Field from '../elements/Field';
import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
import Dialpad from '../voip/DialPad';
@ -72,6 +71,7 @@ import Spinner from "../elements/Spinner";
import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import CallHandler from "../../../CallHandler";
import { logger } from "matrix-js-sdk/src/logger";
@ -806,19 +806,17 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return;
}
dis.dispatch({
action: Action.TransferCallToMatrixID,
call: this.props.call,
destination: targetIds[0],
consultFirst: this.state.consultFirst,
} as TransferCallPayload);
CallHandler.instance.startTransferToMatrixID(
this.props.call,
targetIds[0],
this.state.consultFirst,
);
} else {
dis.dispatch({
action: Action.TransferCallToPhoneNumber,
call: this.props.call,
destination: this.state.dialPadValue,
consultFirst: this.state.consultFirst,
} as TransferCallPayload);
CallHandler.instance.startTransferToPhoneNumber(
this.props.call,
this.state.dialPadValue,
this.state.consultFirst,
);
}
this.props.onFinished();
};

View file

@ -38,6 +38,7 @@ import { MatrixCapabilities } from "matrix-widget-api";
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import CallHandler from '../../../CallHandler';
import { Room } from "matrix-js-sdk/src/models/room";
import { IApp } from "../../../stores/WidgetStore";
import { WidgetLayoutStore, Container } from "../../../stores/widgets/WidgetLayoutStore";
@ -290,7 +291,7 @@ export default class AppTile extends React.Component<IProps, IState> {
}
if (WidgetType.JITSI.matches(this.props.app.type)) {
dis.dispatch({ action: 'hangup_conference' });
CallHandler.instance.hangupCallApp(this.props.room.roomId);
}
// Delete the widget from the persisted store for good measure.

View file

@ -27,7 +27,6 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import { PlaceCallType } from "../../../CallHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Modal from '../../../Modal';
import InfoDialog from "../dialogs/InfoDialog";
@ -36,6 +35,7 @@ import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
import { E2EStatus } from '../../../utils/ShieldUtils';
import { IOOBData } from '../../../stores/ThreepidInviteStore';
import { SearchScope } from './SearchBar';
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { ContextMenuTooltipButton } from '../../structures/ContextMenu';
import RoomContextMenu from "../context_menus/RoomContextMenu";
import { contextMenuBelow } from './RoomTile';
@ -55,7 +55,7 @@ interface IProps {
inRoom: boolean;
onSearchClick: () => void;
onForgetClick: () => void;
onCallPlaced: (type: PlaceCallType) => void;
onCallPlaced: (type: CallType) => void;
onAppsClick: () => void;
e2eStatus: E2EStatus;
appsShown: boolean;
@ -213,14 +213,14 @@ export default class RoomHeader extends React.Component<IProps, IState> {
if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
const voiceCallButton = <AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
onClick={() => this.props.onCallPlaced(CallType.Voice)}
title={_t("Voice call")}
key="voice"
/>;
const videoCallButton = <AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={(ev: React.MouseEvent<Element>) => ev.shiftKey ?
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(CallType.Voice)}
title={_t("Video call")}
key="video"
/>;

View file

@ -20,8 +20,6 @@ import React from 'react';
import CallView from "./CallView";
import RoomViewStore from '../../../stores/RoomViewStore';
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
import dis from '../../../dispatcher/dispatcher';
import { ActionPayload } from '../../../dispatcher/payloads';
import PersistentApp from "../elements/PersistentApp";
import SettingsStore from "../../../settings/SettingsStore";
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
@ -29,6 +27,8 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { EventSubscription } from 'fbemitter';
import PictureInPictureDragger from './PictureInPictureDragger';
import dis from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
import { logger } from "matrix-js-sdk/src/logger";
import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
@ -61,7 +61,7 @@ interface IState {
// The primary will be the one not on hold, or an arbitrary one
// if they're all on hold)
function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall[]] {
const calls = CallHandler.sharedInstance().getAllActiveCallsForPip(roomId);
const calls = CallHandler.instance.getAllActiveCallsForPip(roomId);
let primary: MatrixCall = null;
let secondaries: MatrixCall[] = [];
@ -114,9 +114,9 @@ export default class CallPreview extends React.Component<IProps, IState> {
}
public componentDidMount() {
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCalls);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
const room = MatrixClientPeg.get()?.getRoom(this.state.roomId);
if (room) {
@ -125,12 +125,12 @@ export default class CallPreview extends React.Component<IProps, IState> {
}
public componentWillUnmount() {
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCalls);
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
dis.unregister(this.dispatcherRef);
SettingsStore.unwatchSetting(this.settingsWatcherRef);
const room = MatrixClientPeg.get().getRoom(this.state.roomId);
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
@ -160,19 +160,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
});
};
private onAction = (payload: ActionPayload) => {
switch (payload.action) {
case 'call_state': {
// listen for call state changes to prod the render method, which
// may hide the global CallView if the call it is tracking is dead
this.updateCalls();
break;
}
}
};
private updateCalls = () => {
private updateCalls = (): void => {
if (!this.state.roomId) return;
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.roomId);
@ -194,7 +182,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
private onDoubleClick = (): void => {
dis.dispatch({
action: "view_room",
action: Action.ViewRoom,
room_id: this.state.primaryCall.roomId,
});
};

View file

@ -328,20 +328,17 @@ export default class CallView extends React.Component<IProps, IState> {
};
private onCallResumeClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
const userFacingRoomId = CallHandler.instance.roomIdForCall(this.props.call);
CallHandler.instance.setActiveCallRoomId(userFacingRoomId);
};
private onTransferClick = (): void => {
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
const transfereeCall = CallHandler.instance.getTransfereeForCallId(this.props.call.callId);
this.props.call.transferToCall(transfereeCall);
};
private onHangupClick = (): void => {
dis.dispatch({
action: 'hangup',
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
});
CallHandler.instance.hangupOrReject(CallHandler.instance.roomIdForCall(this.props.call));
};
private onToggleSidebar = (): void => {
@ -404,12 +401,12 @@ export default class CallView extends React.Component<IProps, IState> {
public render() {
const client = MatrixClientPeg.get();
const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
const callRoomId = CallHandler.instance.roomIdForCall(this.props.call);
const secondaryCallRoomId = CallHandler.instance.roomIdForCall(this.props.secondaryCall);
const callRoom = client.getRoom(callRoomId);
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
const avatarSize = this.props.pipMode ? 76 : 160;
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
const transfereeCall = CallHandler.instance.getTransfereeForCallId(this.props.call.callId);
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
const isScreensharing = this.props.call.isScreensharing();
const sidebarShown = this.state.sidebarShown;
@ -423,12 +420,12 @@ export default class CallView extends React.Component<IProps, IState> {
if (transfereeCall) {
const transferTargetRoom = MatrixClientPeg.get().getRoom(
CallHandler.sharedInstance().roomIdForCall(this.props.call),
CallHandler.instance.roomIdForCall(this.props.call),
);
const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
const transfereeRoom = MatrixClientPeg.get().getRoom(
CallHandler.sharedInstance().roomIdForCall(transfereeCall),
CallHandler.instance.roomIdForCall(transfereeCall),
);
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
@ -449,7 +446,7 @@ export default class CallView extends React.Component<IProps, IState> {
} else if (isOnHold) {
let onHoldText = null;
if (this.state.isRemoteOnHold) {
const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
const holdString = CallHandler.instance.hasAnyUnheldCall() ?
_td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
onHoldText = _t(holdString, {}, {
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>

View file

@ -18,7 +18,6 @@ import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import React from 'react';
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
import CallView from './CallView';
import dis from '../../../dispatcher/dispatcher';
import { Resizable } from "re-resizable";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -52,24 +51,15 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
}
public componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCall);
CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
}
public componentWillUnmount() {
dis.unregister(this.dispatcherRef);
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCall);
CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
}
private onAction = (payload) => {
switch (payload.action) {
case 'call_state': {
this.updateCall();
break;
}
}
};
private updateCall = () => {
const newCall = this.getCall();
if (newCall !== this.state.call) {
@ -78,7 +68,7 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
};
private getCall(): MatrixCall {
const call = CallHandler.sharedInstance().getCallForRoom(this.props.roomId);
const call = CallHandler.instance.getCallForRoom(this.props.roomId);
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
return call;

View file

@ -19,11 +19,9 @@ import { createRef } from "react";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import Field from "../elements/Field";
import DialPad from './DialPad';
import dis from '../../../dispatcher/dispatcher';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
import { Action } from "../../../dispatcher/actions";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import CallHandler from "../../../CallHandler";
interface IProps {
onFinished: (boolean) => void;
@ -81,12 +79,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
};
onDialPress = async () => {
const payload: DialNumberPayload = {
action: Action.DialNumber,
number: this.state.value,
};
dis.dispatch(payload);
CallHandler.instance.dialNumber(this.state.value);
this.props.onFinished(true);
};

View file

@ -123,18 +123,6 @@ export enum Action {
*/
DialNumber = "dial_number",
/**
* Start a call transfer to a Matrix ID
* payload: TransferCallPayload
*/
TransferCallToMatrixID = "transfer_call_to_matrix_id",
/**
* Start a call transfer to a phone number
* payload: TransferCallPayload
*/
TransferCallToPhoneNumber = "transfer_call_to_phone_number",
/**
* Fired when CallHandler has checked for PSTN protocol support
* payload: none

View file

@ -1,23 +0,0 @@
/*
Copyright 2021 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 { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface DialNumberPayload extends ActionPayload {
action: Action.DialNumber;
number: string;
}

View file

@ -1,33 +0,0 @@
/*
Copyright 2021 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 { ActionPayload } from "../payloads";
import { Action } from "../actions";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
export interface TransferCallPayload extends ActionPayload {
action: Action.TransferCallToMatrixID | Action.TransferCallToPhoneNumber;
// The call to transfer
call: MatrixCall;
// Where to transfer the call. A Matrix ID if action == TransferCallToMatrixID
// and a phone number if action == TransferCallToPhoneNumber
destination: string;
// If true, puts the current call on hold and dials the transfer target, giving
// the user a button to complete the transfer when ready.
// If false, ends the call immediately and sends the user to the transfer
// destination
consultFirst: boolean;
}

View file

@ -43,7 +43,7 @@ export class VisibilityProvider {
}
if (
CallHandler.sharedInstance().getSupportsVirtualRooms() &&
CallHandler.instance.getSupportsVirtualRooms() &&
VoipUserMapper.sharedInstance().isVirtualRoom(room)
) {
return false;

View file

@ -22,7 +22,6 @@ import { CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import classNames from 'classnames';
import { replaceableComponent } from '../utils/replaceableComponent';
import CallHandler, { CallHandlerEvent } from '../CallHandler';
import dis from '../dispatcher/dispatcher';
import { MatrixClientPeg } from '../MatrixClientPeg';
import { _t } from '../languageHandler';
import RoomAvatar from '../components/views/avatars/RoomAvatar';
@ -45,49 +44,43 @@ export default class IncomingCallToast extends React.Component<IProps, IState> {
super(props);
this.state = {
silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId),
silenced: CallHandler.instance.isCallSilenced(this.props.call.callId),
};
}
public componentDidMount = (): void => {
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
CallHandler.instance.addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
};
public componentWillUnmount(): void {
CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
CallHandler.instance.removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
}
private onSilencedCallsChanged = (): void => {
this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId) });
this.setState({ silenced: CallHandler.instance.isCallSilenced(this.props.call.callId) });
};
private onAnswerClick = (e: React.MouseEvent): void => {
e.stopPropagation();
dis.dispatch({
action: 'answer',
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
});
CallHandler.instance.answerCall(CallHandler.instance.roomIdForCall(this.props.call));
};
private onRejectClick= (e: React.MouseEvent): void => {
e.stopPropagation();
dis.dispatch({
action: 'reject',
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
});
CallHandler.instance.hangupOrReject(CallHandler.instance.roomIdForCall(this.props.call), true);
};
private onSilenceClick = (e: React.MouseEvent): void => {
e.stopPropagation();
const callId = this.props.call.callId;
this.state.silenced ?
CallHandler.sharedInstance().unSilenceCall(callId) :
CallHandler.sharedInstance().silenceCall(callId);
CallHandler.instance.unSilenceCall(callId) :
CallHandler.instance.silenceCall(callId);
};
public render() {
const call = this.props.call;
const room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call));
const room = MatrixClientPeg.get().getRoom(CallHandler.instance.roomIdForCall(call));
const isVoice = call.type === CallType.Voice;
const contentClass = classNames("mx_IncomingCallToast_content", {

View file

@ -16,16 +16,16 @@ limitations under the License.
import './skinned-sdk';
import CallHandler, { PlaceCallType, CallHandlerEvent } from '../src/CallHandler';
import CallHandler, { CallHandlerEvent } from '../src/CallHandler';
import { stubClient, mkStubRoom } from './test-utils';
import { MatrixClientPeg } from '../src/MatrixClientPeg';
import dis from '../src/dispatcher/dispatcher';
import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call';
import { CallEvent, CallState, CallType } from 'matrix-js-sdk/src/webrtc/call';
import DMRoomMap from '../src/utils/DMRoomMap';
import EventEmitter from 'events';
import SdkConfig from '../src/SdkConfig';
import { ActionPayload } from '../src/dispatcher/payloads';
import { Action } from '../src/dispatcher/actions';
import { Action } from "../src/dispatcher/actions";
const REAL_ROOM_ID = '$room1:example.org';
const MAPPED_ROOM_ID = '$room2:example.org';
@ -88,6 +88,14 @@ function untilDispatch(waitForAction: string): Promise<ActionPayload> {
});
}
function untilCallHandlerEvent(callHandler: CallHandler, event: CallHandlerEvent): Promise<void> {
return new Promise<void>((resolve) => {
callHandler.addListener(event, () => {
resolve();
});
});
}
describe('CallHandler', () => {
let dmRoomMap;
let callHandler;
@ -173,10 +181,7 @@ describe('CallHandler', () => {
},
}]);
dis.dispatch({
action: Action.DialNumber,
number: '01818118181',
}, true);
await callHandler.dialNumber('01818118181');
const viewRoomPayload = await untilDispatch(Action.ViewRoom);
expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID);
@ -186,14 +191,9 @@ describe('CallHandler', () => {
});
it('should move calls between rooms when remote asserted identity changes', async () => {
dis.dispatch({
action: 'place_call',
type: PlaceCallType.Voice,
room_id: REAL_ROOM_ID,
}, true);
callHandler.placeCall(REAL_ROOM_ID, CallType.Voice);
// wait for the call to be set up
await untilDispatch('call_state');
await untilCallHandlerEvent(callHandler, CallHandlerEvent.CallState);
// should start off in the actual room ID it's in at the protocol level
expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall);