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. 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 React from 'react';
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
@ -65,7 +28,6 @@ import SettingsStore from './settings/SettingsStore';
import { Jitsi } from "./widgets/Jitsi"; import { Jitsi } from "./widgets/Jitsi";
import { WidgetType } from "./widgets/WidgetType"; import { WidgetType } from "./widgets/WidgetType";
import { SettingLevel } from "./settings/SettingLevel"; import { SettingLevel } from "./settings/SettingLevel";
import { ActionPayload } from "./dispatcher/payloads";
import { base32 } from "rfc4648"; import { base32 } from "rfc4648";
import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import QuestionDialog from "./components/views/dialogs/QuestionDialog";
@ -133,24 +95,24 @@ interface ThirdpartyLookupResponse {
fields: ThirdpartyLookupResponseFields; fields: ThirdpartyLookupResponseFields;
} }
export enum PlaceCallType {
Voice = 'voice',
Video = 'video',
}
export enum CallHandlerEvent { export enum CallHandlerEvent {
CallsChanged = "calls_changed", CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room", CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed", 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 { export default class CallHandler extends EventEmitter {
private calls = new Map<string, MatrixCall>(); // roomId -> call private calls = new Map<string, MatrixCall>(); // roomId -> call
// Calls started as an attended transfer, ie. with the intention of transferring another // Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one. // call with a different party to this one.
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee) private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
private audioPromises = new Map<AudioID, Promise<void>>(); private audioPromises = new Map<AudioID, Promise<void>>();
private dispatcherRef: string = null;
private supportsPstnProtocol = null; private supportsPstnProtocol = null;
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol 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 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 private silencedCalls = new Set<string>(); // callIds
static sharedInstance() { public static get instance() {
if (!window.mxCallHandler) { if (!window.mxCallHandler) {
window.mxCallHandler = new CallHandler(); window.mxCallHandler = new CallHandler();
} }
@ -194,8 +156,7 @@ export default class CallHandler extends EventEmitter {
return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId; return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId;
} }
start() { public start(): void {
this.dispatcherRef = dis.register(this.onAction);
// add empty handlers for media actions, otherwise the media keys // add empty handlers for media actions, otherwise the media keys
// end up causing the audio elements with our ring/ringback etc // end up causing the audio elements with our ring/ringback etc
// audio clips in to play. // audio clips in to play.
@ -215,18 +176,14 @@ export default class CallHandler extends EventEmitter {
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
} }
stop() { public stop(): void {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener('Call.incoming', this.onCallIncoming); 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.silencedCalls.add(callId);
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
@ -235,7 +192,7 @@ export default class CallHandler extends EventEmitter {
this.pause(AudioID.Ring); this.pause(AudioID.Ring);
} }
public unSilenceCall(callId: string) { public unSilenceCall(callId: string): void {
this.silencedCalls.delete(callId); this.silencedCalls.delete(callId);
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
this.play(AudioID.Ring); this.play(AudioID.Ring);
@ -261,7 +218,7 @@ export default class CallHandler extends EventEmitter {
return false; return false;
} }
private async checkProtocols(maxTries) { private async checkProtocols(maxTries: number): Promise<void> {
try { try {
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
@ -296,11 +253,11 @@ export default class CallHandler extends EventEmitter {
} }
} }
public getSupportsPstnProtocol() { public getSupportsPstnProtocol(): boolean {
return this.supportsPstnProtocol; return this.supportsPstnProtocol;
} }
public getSupportsVirtualRooms() { public getSupportsVirtualRooms(): boolean {
return this.supportsSipNativeVirtual; return this.supportsSipNativeVirtual;
} }
@ -328,14 +285,32 @@ export default class CallHandler extends EventEmitter {
); );
} }
private onCallIncoming = (call) => { private onCallIncoming = (call: MatrixCall): void => {
// we dispatch this synchronously to make sure that the event // if the runtime env doesn't do VoIP, stop here.
// handlers on the call are set up immediately (so that if if (!MatrixClientPeg.get().supportsVoip()) {
// we get an immediate hangup, we don't get a stuck call) return;
dis.dispatch({ }
action: 'incoming_call',
call: call, const mappedRoomId = CallHandler.instance.roomIdForCall(call);
}, true); 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 { public getCallById(callId: string): MatrixCall {
@ -345,11 +320,11 @@ export default class CallHandler extends EventEmitter {
return null; return null;
} }
getCallForRoom(roomId: string): MatrixCall { public getCallForRoom(roomId: string): MatrixCall | null {
return this.calls.get(roomId) || null; return this.calls.get(roomId) || null;
} }
getAnyActiveCall() { public getAnyActiveCall(): MatrixCall | null {
for (const call of this.calls.values()) { for (const call of this.calls.values()) {
if (call.state !== CallState.Ended) { if (call.state !== CallState.Ended) {
return call; return call;
@ -358,7 +333,7 @@ export default class CallHandler extends EventEmitter {
return null; return null;
} }
getAllActiveCalls() { public getAllActiveCalls(): MatrixCall[] {
const activeCalls = []; const activeCalls = [];
for (const call of this.calls.values()) { for (const call of this.calls.values()) {
@ -369,7 +344,7 @@ export default class CallHandler extends EventEmitter {
return activeCalls; return activeCalls;
} }
getAllActiveCallsNotInRoom(notInThisRoomId) { public getAllActiveCallsNotInRoom(notInThisRoomId: string): MatrixCall[] {
const callsNotInThatRoom = []; const callsNotInThatRoom = [];
for (const [roomId, call] of this.calls.entries()) { for (const [roomId, call] of this.calls.entries()) {
@ -390,11 +365,11 @@ export default class CallHandler extends EventEmitter {
return this.getAllActiveCallsNotInRoom(roomId); return this.getAllActiveCallsNotInRoom(roomId);
} }
getTransfereeForCallId(callId: string): MatrixCall { public getTransfereeForCallId(callId: string): MatrixCall {
return this.transferees[callId]; return this.transferees[callId];
} }
play(audioId: AudioID) { public play(audioId: AudioID): void {
// TODO: Attach an invisible element for this instead // TODO: Attach an invisible element for this instead
// which listens? // which listens?
const audio = document.getElementById(audioId) as HTMLMediaElement; 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 // TODO: Attach an invisible element for this instead
// which listens? // which listens?
const audio = document.getElementById(audioId) as HTMLMediaElement; 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 // 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 // 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. // 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; return callForThisRoom && call.callId === callForThisRoom.callId;
} }
private setCallListeners(call: MatrixCall) { private setCallListeners(call: MatrixCall): void {
let mappedRoomId = this.roomIdForCall(call); let mappedRoomId = this.roomIdForCall(call);
call.on(CallEvent.Error, (err: CallError) => { call.on(CallEvent.Error, (err: CallError) => {
@ -542,6 +517,11 @@ export default class CallHandler extends EventEmitter {
const mappedRoomId = this.roomIdForCall(call); const mappedRoomId = this.roomIdForCall(call);
this.setCallState(call, newState); this.setCallState(call, newState);
dis.dispatch({
action: 'call_state',
room_id: mappedRoomId,
state: newState,
});
switch (oldState) { switch (oldState) {
case CallState.Ringing: 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(); const stats = await call.getCurrentCallStats();
logger.debug( logger.debug(
`Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` + `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) { private setCallState(call: MatrixCall, status: CallState): void {
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); const mappedRoomId = CallHandler.instance.roomIdForCall(call);
logger.log( logger.log(
`Call state in ${mappedRoomId} changed to ${status}`, `Call state in ${mappedRoomId} changed to ${status}`,
@ -683,20 +663,16 @@ export default class CallHandler extends EventEmitter {
ToastStore.sharedInstance().dismissToast(toastKey); ToastStore.sharedInstance().dismissToast(toastKey);
} }
dis.dispatch({ this.emit(CallHandlerEvent.CallState, mappedRoomId, status);
action: 'call_state',
room_id: mappedRoomId,
state: status,
});
} }
private removeCallForRoom(roomId: string) { private removeCallForRoom(roomId: string): void {
logger.log("Removing call for room ", roomId); logger.log("Removing call for room ", roomId);
this.calls.delete(roomId); this.calls.delete(roomId);
this.emit(CallHandlerEvent.CallsChanged, this.calls); this.emit(CallHandlerEvent.CallsChanged, this.calls);
} }
private showICEFallbackPrompt() { private showICEFallbackPrompt(): void {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const code = sub => <code>{ sub }</code>; const code = sub => <code>{ sub }</code>;
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
@ -725,7 +701,7 @@ export default class CallHandler extends EventEmitter {
}, null, true); }, null, true);
} }
private showMediaCaptureError(call: MatrixCall) { private showMediaCaptureError(call: MatrixCall): void {
let title; let title;
let description; let description;
@ -754,9 +730,9 @@ export default class CallHandler extends EventEmitter {
}, null, true); }, 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); 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; const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId;
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
@ -782,7 +758,7 @@ export default class CallHandler extends EventEmitter {
this.setActiveCallRoomId(roomId); this.setActiveCallRoomId(roomId);
if (type === PlaceCallType.Voice) { if (type === CallType.Voice) {
call.placeVoiceCall(); call.placeVoiceCall();
} else if (type === 'video') { } else if (type === 'video') {
call.placeVideoCall(); call.placeVideoCall();
@ -791,166 +767,102 @@ export default class CallHandler extends EventEmitter {
} }
} }
private onAction = (payload: ActionPayload) => { public placeCall(roomId: string, type?: CallType, transferee?: MatrixCall): void {
switch (payload.action) { // We might be using managed hybrid widgets
case 'place_call': if (isManagedHybridWidgetEnabled()) {
{ addManagedHybridWidget(roomId);
// We might be using managed hybrid widgets return;
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;
} }
};
// 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 { private stopRingingIfPossible(callId: string): void {
this.silencedCalls.delete(callId); this.silencedCalls.delete(callId);
@ -958,7 +870,7 @@ export default class CallHandler extends EventEmitter {
this.pause(AudioID.Ring); this.pause(AudioID.Ring);
} }
private async dialNumber(number: string) { public async dialNumber(number: string): Promise<void> {
const results = await this.pstnLookup(number); const results = await this.pstnLookup(number);
if (!results || results.length === 0 || !results[0].userid) { if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, { Modal.createTrackedDialog('', '', ErrorDialog, {
@ -988,10 +900,12 @@ export default class CallHandler extends EventEmitter {
room_id: roomId, 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); const results = await this.pstnLookup(destination);
if (!results || results.length === 0 || !results[0].userid) { if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, { Modal.createTrackedDialog('', '', ErrorDialog, {
@ -1004,16 +918,13 @@ export default class CallHandler extends EventEmitter {
await this.startTransferToMatrixID(call, results[0].userid, consultFirst); 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) { if (consultFirst) {
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination); const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination);
dis.dispatch({ this.placeCall(dmRoomId, call.type, call);
action: 'place_call',
type: call.type,
room_id: dmRoomId,
transferee: call,
});
dis.dispatch({ dis.dispatch({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: dmRoomId, 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"); logger.info("Setting call in room " + activeCallRoomId + " active");
for (const [roomId, call] of this.calls.entries()) { 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 * @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()) { for (const call of this.calls.values()) {
if (call.state === CallState.Ended) continue; if (call.state === CallState.Ended) continue;
if (!call.isRemoteOnHold()) return true; if (!call.isRemoteOnHold()) return true;
@ -1060,7 +971,11 @@ export default class CallHandler extends EventEmitter {
return false; 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({ dis.dispatch({
action: 'appsDrawer', action: 'appsDrawer',
show: true, 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, { Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
hasCancelButton: true, hasCancelButton: true,
title: _t("End conference"), 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); const roomInfo = WidgetStore.instance.getRoom(roomId);
if (!roomInfo) return; // "should never happen" clauses go here 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(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.instance.start(); ActiveWidgetStore.instance.start();
CallHandler.sharedInstance().start(); CallHandler.instance.start();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting // 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 // 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 { export function stopMatrixClient(unsetClient = true): void {
Notifier.stop(); Notifier.stop();
CallHandler.sharedInstance().stop(); CallHandler.instance.stop();
UserActivity.sharedInstance().stop(); UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset(); TypingStore.sharedInstance().reset();
Presence.stop(); Presence.stop();

View file

@ -1037,7 +1037,7 @@ export const Commands = [
return success((async () => { return success((async () => {
if (isPhoneNumber) { 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) { if (!results || results.length === 0 || !results[0].userid) {
throw new Error("Unable to find Matrix ID for phone number"); 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"), description: _td("Places the call in the current room on hold"),
category: CommandCategories.other, category: CommandCategories.other,
runFn: function(roomId, args) { runFn: function(roomId, args) {
const call = CallHandler.sharedInstance().getCallForRoom(roomId); const call = CallHandler.instance.getCallForRoom(roomId);
if (!call) { if (!call) {
return reject("No active call in this room"); 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"), description: _td("Takes the call in the current room off hold"),
category: CommandCategories.other, category: CommandCategories.other,
runFn: function(roomId, args) { runFn: function(roomId, args) {
const call = CallHandler.sharedInstance().getCallForRoom(roomId); const call = CallHandler.instance.getCallForRoom(roomId);
if (!call) { if (!call) {
return reject("No active call in this room"); 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> { 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; if (results.length === 0 || !results[0].fields.lookup_success) return null;
return results[0].userid; return results[0].userid;
} }
@ -97,11 +97,11 @@ export default class VoipUserMapper {
} }
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> { public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; if (!CallHandler.instance.getSupportsVirtualRooms()) return;
const inviterId = invitedRoom.getDMInviter(); const inviterId = invitedRoom.getDMInviter();
logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); 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) { if (result.length === 0) {
return; return;
} }

View file

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

View file

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

View file

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

View file

@ -34,7 +34,7 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import ResizeNotifier from '../../utils/ResizeNotifier'; import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages'; import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal'; import Modal from '../../Modal';
import CallHandler, { PlaceCallType } from '../../CallHandler'; import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching'; import eventSearch, { searchPagination } from '../../Searching';
@ -66,7 +66,7 @@ import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay"; import EffectsOverlay from "../views/elements/EffectsOverlay";
import { containsEmoji } from '../../effects/utils'; import { containsEmoji } from '../../effects/utils';
import { CHAT_EFFECTS } from '../../effects'; 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 WidgetStore from "../../stores/WidgetStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import Notifier from "../../Notifier"; import Notifier from "../../Notifier";
@ -669,6 +669,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
componentDidUpdate() { componentDidUpdate() {
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.onCallState);
if (this.roomView.current) { if (this.roomView.current) {
const roomView = this.roomView.current; const roomView = this.roomView.current;
if (!roomView.ondrop) { if (!roomView.ondrop) {
@ -699,6 +700,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// (We could use isMounted, but facebook have deprecated that.) // (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true; this.unmounted = true;
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.onCallState);
// update the scroll map before we get unmounted // update the scroll map before we get unmounted
if (this.state.roomId) { if (this.state.roomId) {
RoomScrollStateStore.setScrollState(this.state.roomId, this.getScrollState()); 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 => { private onAction = payload => {
switch (payload.action) { switch (payload.action) {
case 'message_sent': case 'message_sent':
@ -843,21 +855,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
case Action.UploadCanceled: case Action.UploadCanceled:
this.forceUpdate(); this.forceUpdate();
break; 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': case 'appsDrawer':
this.setState({ this.setState({
showApps: payload.show, showApps: payload.show,
@ -1531,12 +1528,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return ret; return ret;
} }
private onCallPlaced = (type: PlaceCallType) => { private onCallPlaced = (type: CallType): void => {
dis.dispatch({ CallHandler.instance.placeCall(this.state.room?.roomId, type);
action: 'place_call',
type: type,
room_id: this.state.room.roomId,
});
}; };
private onAppsClick = () => { private onAppsClick = () => {
@ -1748,7 +1741,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room) { if (!this.state.room) {
return null; 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, // 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 = () => { onUnholdClick = () => {
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId); CallHandler.instance.setActiveCallRoomId(this.props.call.roomId);
this.props.onFinished(); this.props.onFinished();
}; };

View file

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

View file

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

View file

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

View file

@ -328,20 +328,17 @@ export default class CallView extends React.Component<IProps, IState> {
}; };
private onCallResumeClick = (): void => { private onCallResumeClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); const userFacingRoomId = CallHandler.instance.roomIdForCall(this.props.call);
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId); CallHandler.instance.setActiveCallRoomId(userFacingRoomId);
}; };
private onTransferClick = (): void => { 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); this.props.call.transferToCall(transfereeCall);
}; };
private onHangupClick = (): void => { private onHangupClick = (): void => {
dis.dispatch({ CallHandler.instance.hangupOrReject(CallHandler.instance.roomIdForCall(this.props.call));
action: 'hangup',
room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
});
}; };
private onToggleSidebar = (): void => { private onToggleSidebar = (): void => {
@ -404,12 +401,12 @@ export default class CallView extends React.Component<IProps, IState> {
public render() { public render() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); const callRoomId = CallHandler.instance.roomIdForCall(this.props.call);
const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall); const secondaryCallRoomId = CallHandler.instance.roomIdForCall(this.props.secondaryCall);
const callRoom = client.getRoom(callRoomId); const callRoom = client.getRoom(callRoomId);
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null; const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
const avatarSize = this.props.pipMode ? 76 : 160; 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 isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
const isScreensharing = this.props.call.isScreensharing(); const isScreensharing = this.props.call.isScreensharing();
const sidebarShown = this.state.sidebarShown; const sidebarShown = this.state.sidebarShown;
@ -423,12 +420,12 @@ export default class CallView extends React.Component<IProps, IState> {
if (transfereeCall) { if (transfereeCall) {
const transferTargetRoom = MatrixClientPeg.get().getRoom( 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 transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
const transfereeRoom = MatrixClientPeg.get().getRoom( const transfereeRoom = MatrixClientPeg.get().getRoom(
CallHandler.sharedInstance().roomIdForCall(transfereeCall), CallHandler.instance.roomIdForCall(transfereeCall),
); );
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person"); const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
@ -449,7 +446,7 @@ export default class CallView extends React.Component<IProps, IState> {
} else if (isOnHold) { } else if (isOnHold) {
let onHoldText = null; let onHoldText = null;
if (this.state.isRemoteOnHold) { 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>"); _td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
onHoldText = _t(holdString, {}, { onHoldText = _t(holdString, {}, {
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}> 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 React from 'react';
import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
import CallView from './CallView'; import CallView from './CallView';
import dis from '../../../dispatcher/dispatcher';
import { Resizable } from "re-resizable"; import { Resizable } from "re-resizable";
import ResizeNotifier from "../../../utils/ResizeNotifier"; import ResizeNotifier from "../../../utils/ResizeNotifier";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -52,24 +51,15 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
} }
public componentDidMount() { public componentDidMount() {
this.dispatcherRef = dis.register(this.onAction); CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCall);
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCall); CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
} }
public componentWillUnmount() { public componentWillUnmount() {
dis.unregister(this.dispatcherRef); CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCall);
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall); CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
} }
private onAction = (payload) => {
switch (payload.action) {
case 'call_state': {
this.updateCall();
break;
}
}
};
private updateCall = () => { private updateCall = () => {
const newCall = this.getCall(); const newCall = this.getCall();
if (newCall !== this.state.call) { if (newCall !== this.state.call) {
@ -78,7 +68,7 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
}; };
private getCall(): MatrixCall { 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; if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
return call; return call;

View file

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

View file

@ -123,18 +123,6 @@ export enum Action {
*/ */
DialNumber = "dial_number", 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 * Fired when CallHandler has checked for PSTN protocol support
* payload: none * 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 ( if (
CallHandler.sharedInstance().getSupportsVirtualRooms() && CallHandler.instance.getSupportsVirtualRooms() &&
VoipUserMapper.sharedInstance().isVirtualRoom(room) VoipUserMapper.sharedInstance().isVirtualRoom(room)
) { ) {
return false; return false;

View file

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

View file

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