diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index ed884340a7..eaff6ee186 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -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: - * } - * - * 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: - * } - * - * { - * action: 'incoming_call' - * call: MatrixCall - * } - * - * { - * action: 'hangup' - * room_id: - * } - * - * { - * action: 'answer' - * room_id: - * } - */ - 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(); // 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(); // callId (target) -> call (transferee) private audioPromises = new Map>(); - 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(); // 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 { 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 { 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 => { sub }; 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 { 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 { 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 { 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 { 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 { + 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 diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index d179f9bed3..8477116104 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -794,7 +794,7 @@ async function startMatrixClient(startSyncing = true): Promise { 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 { 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"); } diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index e2e590548e..00b9a1167a 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -36,7 +36,7 @@ export default class VoipUserMapper { } private async userToVirtualUser(userId: string): Promise { - 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 { - 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; } diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 84e004d1de..f2729f9f29 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -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(); }; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index b0a66f4c8c..7c377e168a 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -340,7 +340,7 @@ export default class LeftPanel extends React.Component { // 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 = { // 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 { 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 { 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 { this.resizer.detach(); } + private onCallState = (): void => { + const activeCalls = CallHandler.instance.getAllActiveCalls(); + if (activeCalls === this.state.activeCalls) return; + this.setState({ activeCalls }); + }; + private refreshBackgroundImage = async (): Promise => { let backgroundImage = SettingsStore.getValue("RoomList.backgroundImage"); if (backgroundImage) { @@ -233,18 +240,6 @@ class LoggedInView extends React.Component { 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; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index d7b866ada5..9b423fce82 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -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 { } break; case 'logout': - dis.dispatch({ action: "hangup_all" }); + CallHandler.instance.hangupAllCalls(); Lifecycle.logout(); break; case 'require_registration': diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 936454bbf3..9f4c291adf 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -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 { } 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 { // (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 { } }; + 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 { 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 { 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 { 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, diff --git a/src/components/views/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx index 38398e70b3..7fcf1fa50c 100644 --- a/src/components/views/context_menus/CallContextMenu.tsx +++ b/src/components/views/context_menus/CallContextMenu.tsx @@ -45,7 +45,7 @@ export default class CallContextMenu extends React.Component { }; onUnholdClick = () => { - CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId); + CallHandler.instance.setActiveCallRoomId(this.props.call.roomId); this.props.onFinished(); }; diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 4f28ff5ebd..2c68370921 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -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 { } 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. diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 7bce296e8f..d16feb3ae8 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -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 { if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) { const voiceCallButton = this.props.onCallPlaced(PlaceCallType.Voice)} + onClick={() => this.props.onCallPlaced(CallType.Voice)} title={_t("Voice call")} key="voice" />; const videoCallButton = ) => ev.shiftKey ? - this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)} + this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(CallType.Voice)} title={_t("Video call")} key="video" />; diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 1c667e4229..5de1a83265 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -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 { } 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 { } 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 { }); }; - 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 { private onDoubleClick = (): void => { dis.dispatch({ - action: "view_room", + action: Action.ViewRoom, room_id: this.state.primaryCall.roomId, }); }; diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 2e3ec44755..54dfa01890 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -328,20 +328,17 @@ export default class CallView extends React.Component { }; 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 { 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 { 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 { } 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 Switch") : _td("You held the call Resume"); onHoldText = _t(holdString, {}, { a: sub => diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx index c63713873a..99b577f4ad 100644 --- a/src/components/views/voip/CallViewForRoom.tsx +++ b/src/components/views/voip/CallViewForRoom.tsx @@ -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 { } 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 { }; 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; diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index 4d69260565..7eaf56196a 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -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 { }; 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); }; diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 22ed1bc516..6291e86a70 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -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 diff --git a/src/dispatcher/payloads/DialNumberPayload.ts b/src/dispatcher/payloads/DialNumberPayload.ts deleted file mode 100644 index 1b591b9f6b..0000000000 --- a/src/dispatcher/payloads/DialNumberPayload.ts +++ /dev/null @@ -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; -} diff --git a/src/dispatcher/payloads/TransferCallPayload.ts b/src/dispatcher/payloads/TransferCallPayload.ts deleted file mode 100644 index 38431bb0d6..0000000000 --- a/src/dispatcher/payloads/TransferCallPayload.ts +++ /dev/null @@ -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; -} diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index 18b68da301..21dff9d319 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -43,7 +43,7 @@ export class VisibilityProvider { } if ( - CallHandler.sharedInstance().getSupportsVirtualRooms() && + CallHandler.instance.getSupportsVirtualRooms() && VoipUserMapper.sharedInstance().isVirtualRoom(room) ) { return false; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index fbe4e66d50..4a31ccbb85 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -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 { 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", { diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index b1e0214e4f..57f592cca8 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -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 { }); } +function untilCallHandlerEvent(callHandler: CallHandler, event: CallHandlerEvent): Promise { + return new Promise((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);