2020-09-24 15:16:20 +00:00
/ *
Copyright 2015 , 2016 OpenMarket Ltd
Copyright 2017 , 2018 New Vector Ltd
2022-03-09 12:05:16 +00:00
Copyright 2019 - 2022 The Matrix . org Foundation C . I . C .
2021-08-03 12:48:39 +00:00
Copyright 2021 Š imon Brandner < simon.bra.ag @ gmail.com >
2020-09-24 15:16:20 +00:00
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 React from "react" ;
2023-08-10 08:01:14 +00:00
import { MatrixError , RuleId , TweakName , SyncState } from "matrix-js-sdk/src/matrix" ;
2022-02-22 12:18:08 +00:00
import {
CallError ,
CallErrorCode ,
CallEvent ,
CallParty ,
CallState ,
CallType ,
2023-06-05 13:53:11 +00:00
FALLBACK_ICE_SERVER ,
2022-02-22 12:18:08 +00:00
MatrixCall ,
} from "matrix-js-sdk/src/webrtc/call" ;
2021-12-09 09:10:23 +00:00
import { logger } from "matrix-js-sdk/src/logger" ;
import EventEmitter from "events" ;
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor" ;
2022-02-22 12:18:08 +00:00
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler" ;
2020-09-24 15:16:20 +00:00
2021-06-29 12:11:58 +00:00
import { MatrixClientPeg } from "./MatrixClientPeg" ;
2020-09-24 15:16:20 +00:00
import Modal from "./Modal" ;
import { _t } from "./languageHandler" ;
import dis from "./dispatcher/dispatcher" ;
import WidgetUtils from "./utils/WidgetUtils" ;
import SettingsStore from "./settings/SettingsStore" ;
2021-06-29 12:11:58 +00:00
import { WidgetType } from "./widgets/WidgetType" ;
import { SettingLevel } from "./settings/SettingLevel" ;
2020-09-24 15:16:20 +00:00
import QuestionDialog from "./components/views/dialogs/QuestionDialog" ;
import ErrorDialog from "./components/views/dialogs/ErrorDialog" ;
2020-09-28 19:53:44 +00:00
import WidgetStore from "./stores/WidgetStore" ;
2020-10-01 02:09:23 +00:00
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore" ;
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions" ;
2021-06-29 12:11:58 +00:00
import { UIFeature } from "./settings/UIFeature" ;
2020-12-23 19:02:01 +00:00
import { Action } from "./dispatcher/actions" ;
2021-02-12 20:55:54 +00:00
import VoipUserMapper from "./VoipUserMapper" ;
2021-01-29 14:26:33 +00:00
import { addManagedHybridWidget , isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid" ;
2021-04-19 19:30:51 +00:00
import SdkConfig from "./SdkConfig" ;
2022-03-24 21:32:22 +00:00
import { ensureDMExists } from "./createRoom" ;
2022-02-22 12:18:08 +00:00
import { Container , WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore" ;
2022-08-30 19:13:39 +00:00
import IncomingLegacyCallToast , { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast" ;
2021-07-24 11:05:14 +00:00
import ToastStore from "./stores/ToastStore" ;
2022-01-20 09:32:15 +00:00
import Resend from "./Resend" ;
2022-02-10 14:29:55 +00:00
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload" ;
2023-02-28 10:31:48 +00:00
import { InviteKind } from "./components/views/dialogs/InviteDialogTypes" ;
2022-03-24 22:30:53 +00:00
import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload" ;
2022-07-25 08:17:40 +00:00
import { findDMForUser } from "./utils/dm/findDMForUser" ;
2022-08-01 17:28:33 +00:00
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers" ;
2022-10-17 09:16:04 +00:00
import { localNotificationsAreSilenced } from "./utils/notifications" ;
2022-12-19 08:44:19 +00:00
import { SdkContextClass } from "./contexts/SDKContext" ;
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog" ;
2023-05-05 16:08:07 +00:00
import { isNotNull } from "./Typeguards" ;
2020-12-23 19:02:01 +00:00
2021-02-12 20:55:54 +00:00
export const PROTOCOL_PSTN = "m.protocol.pstn" ;
export const PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn" ;
export const PROTOCOL_SIP_NATIVE = "im.vector.protocol.sip_native" ;
export const PROTOCOL_SIP_VIRTUAL = "im.vector.protocol.sip_virtual" ;
const CHECK_PROTOCOLS_ATTEMPTS = 3 ;
2020-09-24 15:16:20 +00:00
2022-12-01 04:08:09 +00:00
type MediaEventType = keyof HTMLMediaElementEventMap ;
const MEDIA_ERROR_EVENT_TYPES : MediaEventType [ ] = [
"error" ,
// The media has become empty; for example, this event is sent if the media has
// already been loaded (or partially loaded), and the HTMLMediaElement.load method
// is called to reload it.
"emptied" ,
// The user agent is trying to fetch media data, but data is unexpectedly not
// forthcoming.
"stalled" ,
// Media data loading has been suspended.
"suspend" ,
// Playback has stopped because of a temporary lack of data
"waiting" ,
] ;
const MEDIA_DEBUG_EVENT_TYPES : MediaEventType [ ] = [
"play" ,
"pause" ,
"playing" ,
"ended" ,
"loadeddata" ,
"loadedmetadata" ,
"canplay" ,
"canplaythrough" ,
"volumechange" ,
] ;
const MEDIA_EVENT_TYPES = [ . . . MEDIA_ERROR_EVENT_TYPES , . . . MEDIA_DEBUG_EVENT_TYPES ] ;
export enum AudioID {
2020-10-09 17:56:07 +00:00
Ring = "ringAudio" ,
Ringback = "ringbackAudio" ,
CallEnd = "callendAudio" ,
Busy = "busyAudio" ,
}
2020-09-24 17:18:26 +00:00
2022-12-01 04:08:09 +00:00
/* istanbul ignore next */
const debuglog = ( . . . args : any [ ] ) : void = > {
if ( SettingsStore . getValue ( "debug_legacy_call_handler" ) ) {
logger . log . call ( console , "LegacyCallHandler debuglog:" , . . . args ) ;
}
} ;
2021-02-15 15:25:07 +00:00
interface ThirdpartyLookupResponseFields {
/* eslint-disable camelcase */
// im.vector.sip_native
2021-02-16 18:52:49 +00:00
virtual_mxid? : string ;
is_virtual? : boolean ;
2021-02-15 15:25:07 +00:00
// im.vector.sip_virtual
2021-02-16 18:52:49 +00:00
native_mxid? : string ;
is_native? : boolean ;
2021-02-15 15:25:07 +00:00
// common
2021-02-16 18:52:49 +00:00
lookup_success? : boolean ;
2021-02-15 15:25:07 +00:00
/* eslint-enable camelcase */
}
2021-02-15 15:04:01 +00:00
interface ThirdpartyLookupResponse {
2021-07-01 22:23:03 +00:00
userid : string ;
protocol : string ;
fields : ThirdpartyLookupResponseFields ;
2021-02-12 20:55:54 +00:00
}
2022-08-30 19:13:39 +00:00
export enum LegacyCallHandlerEvent {
2021-04-27 09:01:36 +00:00
CallsChanged = "calls_changed" ,
2021-04-28 09:49:07 +00:00
CallChangeRoom = "call_change_room" ,
2021-06-19 17:30:19 +00:00
SilencedCallsChanged = "silenced_calls_changed" ,
2021-11-30 18:09:13 +00:00
CallState = "call_state" ,
2021-04-27 09:01:36 +00:00
}
2021-11-30 18:09:13 +00:00
/ * *
2022-08-30 19:13:39 +00:00
* LegacyCallHandler manages all currently active calls . It should be used for
2021-11-30 18:09:13 +00:00
* placing , answering , rejecting and hanging up calls . It also handles ringing ,
* PSTN support and other things .
* /
2022-08-30 19:13:39 +00:00
export default class LegacyCallHandler extends EventEmitter {
2020-12-03 17:45:49 +00:00
private calls = new Map < string , MatrixCall > ( ) ; // roomId -> call
2021-03-25 19:56:21 +00:00
// Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one.
private transferees = new Map < string , MatrixCall > ( ) ; // callId (target) -> call (transferee)
2020-10-12 08:55:21 +00:00
private audioPromises = new Map < AudioID , Promise < void > > ( ) ;
2022-12-01 04:08:09 +00:00
private audioElementsWithListeners = new Map < HTMLMediaElement , boolean > ( ) ;
2023-02-13 11:39:16 +00:00
private supportsPstnProtocol : boolean | null = null ;
private pstnSupportPrefixed : boolean | null = null ; // True if the server only support the prefixed pstn protocol
private supportsSipNativeVirtual : boolean | null = null ; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
2020-09-24 15:16:20 +00:00
2021-04-27 17:55:53 +00:00
// Map of the asserted identity users after we've looked them up using the API.
2021-04-19 19:30:51 +00:00
// We need to be be able to determine the mapped room synchronously, so we
// do the async lookup when we get new information and then store these mappings here
private assertedIdentityNativeUsers = new Map < string , string > ( ) ;
2021-07-08 15:16:02 +00:00
private silencedCalls = new Set < string > ( ) ; // callIds
2021-06-19 17:30:19 +00:00
2023-01-12 13:25:14 +00:00
public static get instance ( ) : LegacyCallHandler {
2022-08-30 19:13:39 +00:00
if ( ! window . mxLegacyCallHandler ) {
window . mxLegacyCallHandler = new LegacyCallHandler ( ) ;
2020-09-24 15:16:20 +00:00
}
2022-08-30 19:13:39 +00:00
return window . mxLegacyCallHandler ;
2020-09-24 15:16:20 +00:00
}
2021-01-21 19:20:35 +00:00
/ *
* Gets the user - facing room associated with a call ( call . roomId may be the call "virtual room"
* if a voip_mxid_translate_pattern is set in the config )
* /
2023-02-16 17:21:44 +00:00
public roomIdForCall ( call? : MatrixCall ) : string | null {
2021-01-21 19:20:35 +00:00
if ( ! call ) return null ;
2021-04-19 19:30:51 +00:00
2022-03-02 09:59:01 +00:00
// check asserted identity: if we're not obeying asserted identity,
// this map will never be populated, but we check anyway for sanity
if ( this . shouldObeyAssertedfIdentity ( ) ) {
2023-02-13 11:39:16 +00:00
const nativeUser = this . assertedIdentityNativeUsers . get ( call . callId ) ;
2021-04-19 19:30:51 +00:00
if ( nativeUser ) {
2023-06-21 16:29:44 +00:00
const room = findDMForUser ( MatrixClientPeg . safeGet ( ) , nativeUser ) ;
2021-06-29 12:11:58 +00:00
if ( room ) return room . roomId ;
2021-04-19 19:30:51 +00:00
}
}
2023-02-16 17:21:44 +00:00
return VoipUserMapper . sharedInstance ( ) . nativeRoomForVirtualRoom ( call . roomId ) ? ? call . roomId ? ? null ;
2021-01-21 19:20:35 +00:00
}
2021-11-30 18:09:13 +00:00
public start ( ) : void {
2020-09-24 15:16:20 +00:00
// add empty handlers for media actions, otherwise the media keys
// end up causing the audio elements with our ring/ringback etc
// audio clips in to play.
if ( navigator . mediaSession ) {
navigator . mediaSession . setActionHandler ( "play" , function ( ) { } ) ;
navigator . mediaSession . setActionHandler ( "pause" , function ( ) { } ) ;
navigator . mediaSession . setActionHandler ( "seekbackward" , function ( ) { } ) ;
navigator . mediaSession . setActionHandler ( "seekforward" , function ( ) { } ) ;
navigator . mediaSession . setActionHandler ( "previoustrack" , function ( ) { } ) ;
navigator . mediaSession . setActionHandler ( "nexttrack" , function ( ) { } ) ;
}
2020-11-23 16:20:15 +00:00
if ( SettingsStore . getValue ( UIFeature . Voip ) ) {
2023-06-21 16:29:44 +00:00
MatrixClientPeg . safeGet ( ) . on ( CallEventHandlerEvent . Incoming , this . onCallIncoming ) ;
2020-11-23 16:20:15 +00:00
}
2020-12-23 19:02:01 +00:00
2021-02-12 20:55:54 +00:00
this . checkProtocols ( CHECK_PROTOCOLS_ATTEMPTS ) ;
2022-12-01 04:08:09 +00:00
// Add event listeners for the <audio> elements
Object . values ( AudioID ) . forEach ( ( audioId ) = > {
const audioElement = document . getElementById ( audioId ) as HTMLMediaElement ;
if ( audioElement ) {
this . addEventListenersForAudioElement ( audioElement ) ;
} else {
logger . warn ( ` LegacyCallHandler: missing <audio id=" ${ audioId } "> from page ` ) ;
}
} ) ;
2020-11-23 16:20:15 +00:00
}
2021-11-30 18:09:13 +00:00
public stop ( ) : void {
2020-11-23 16:20:15 +00:00
const cli = MatrixClientPeg . get ( ) ;
if ( cli ) {
2022-02-22 12:18:08 +00:00
cli . removeListener ( CallEventHandlerEvent . Incoming , this . onCallIncoming ) ;
2020-11-23 16:20:15 +00:00
}
2022-12-01 04:08:09 +00:00
// Remove event listeners for the <audio> elements
Array . from ( this . audioElementsWithListeners . keys ( ) ) . forEach ( ( audioElement ) = > {
this . removeEventListenersForAudioElement ( audioElement ) ;
} ) ;
}
private addEventListenersForAudioElement ( audioElement : HTMLMediaElement ) : void {
// Only need to setup the listeners once
if ( ! this . audioElementsWithListeners . get ( audioElement ) ) {
MEDIA_EVENT_TYPES . forEach ( ( errorEventType ) = > {
audioElement . addEventListener ( errorEventType , this ) ;
this . audioElementsWithListeners . set ( audioElement , true ) ;
} ) ;
}
}
private removeEventListenersForAudioElement ( audioElement : HTMLMediaElement ) : void {
MEDIA_EVENT_TYPES . forEach ( ( errorEventType ) = > {
audioElement . removeEventListener ( errorEventType , this ) ;
} ) ;
}
/* istanbul ignore next (remove if we start using this function for things other than debug logging) */
public handleEvent ( e : Event ) : void {
const target = e . target as HTMLElement ;
const audioId = target ? . id ;
if ( MEDIA_ERROR_EVENT_TYPES . includes ( e . type as MediaEventType ) ) {
logger . error ( ` LegacyCallHandler: encountered " ${ e . type } " event with <audio id=" ${ audioId } "> ` , e ) ;
} else if ( MEDIA_EVENT_TYPES . includes ( e . type as MediaEventType ) ) {
debuglog ( ` encountered " ${ e . type } " event with <audio id=" ${ audioId } "> ` , e ) ;
}
2020-11-23 16:20:15 +00:00
}
2022-10-17 09:16:04 +00:00
public isForcedSilent ( ) : boolean {
2023-06-21 16:29:44 +00:00
const cli = MatrixClientPeg . safeGet ( ) ;
2022-10-17 09:16:04 +00:00
return localNotificationsAreSilenced ( cli ) ;
}
2023-04-28 08:45:36 +00:00
public silenceCall ( callId? : string ) : void {
if ( ! callId ) return ;
2021-07-08 15:16:02 +00:00
this . silencedCalls . add ( callId ) ;
2022-08-30 19:13:39 +00:00
this . emit ( LegacyCallHandlerEvent . SilencedCallsChanged , this . silencedCalls ) ;
2021-06-19 17:30:19 +00:00
// Don't pause audio if we have calls which are still ringing
if ( this . areAnyCallsUnsilenced ( ) ) return ;
this . pause ( AudioID . Ring ) ;
}
2023-04-28 08:45:36 +00:00
public unSilenceCall ( callId? : string ) : void {
if ( ! callId || this . isForcedSilent ( ) ) return ;
2021-07-08 15:16:02 +00:00
this . silencedCalls . delete ( callId ) ;
2022-08-30 19:13:39 +00:00
this . emit ( LegacyCallHandlerEvent . SilencedCallsChanged , this . silencedCalls ) ;
2021-06-19 17:30:19 +00:00
this . play ( AudioID . Ring ) ;
}
2023-04-20 08:49:10 +00:00
public isCallSilenced ( callId? : string ) : boolean {
return this . isForcedSilent ( ) || ( ! ! callId && this . silencedCalls . has ( callId ) ) ;
2021-06-19 17:30:19 +00:00
}
/ * *
* Returns true if there is at least one unsilenced call
* @returns { boolean }
* /
private areAnyCallsUnsilenced ( ) : boolean {
2021-08-26 12:59:06 +00:00
for ( const call of this . calls . values ( ) ) {
if ( call . state === CallState . Ringing && ! this . isCallSilenced ( call . callId ) ) {
return true ;
}
}
return false ;
2021-06-19 17:30:19 +00:00
}
2021-11-30 18:09:13 +00:00
private async checkProtocols ( maxTries : number ) : Promise < void > {
2020-12-23 19:02:01 +00:00
try {
2023-06-21 16:29:44 +00:00
const protocols = await MatrixClientPeg . safeGet ( ) . getThirdpartyProtocols ( ) ;
2021-02-12 20:55:54 +00:00
if ( protocols [ PROTOCOL_PSTN ] !== undefined ) {
this . supportsPstnProtocol = Boolean ( protocols [ PROTOCOL_PSTN ] ) ;
if ( this . supportsPstnProtocol ) this . pstnSupportPrefixed = false ;
} else if ( protocols [ PROTOCOL_PSTN_PREFIXED ] !== undefined ) {
this . supportsPstnProtocol = Boolean ( protocols [ PROTOCOL_PSTN_PREFIXED ] ) ;
if ( this . supportsPstnProtocol ) this . pstnSupportPrefixed = true ;
2020-12-23 19:02:01 +00:00
} else {
this . supportsPstnProtocol = null ;
}
2021-02-12 20:55:54 +00:00
2021-06-29 12:11:58 +00:00
dis . dispatch ( { action : Action.PstnSupportUpdated } ) ;
2021-02-12 20:55:54 +00:00
if ( protocols [ PROTOCOL_SIP_NATIVE ] !== undefined && protocols [ PROTOCOL_SIP_VIRTUAL ] !== undefined ) {
this . supportsSipNativeVirtual = Boolean (
protocols [ PROTOCOL_SIP_NATIVE ] && protocols [ PROTOCOL_SIP_VIRTUAL ] ,
) ;
}
2021-06-29 12:11:58 +00:00
dis . dispatch ( { action : Action.VirtualRoomSupportUpdated } ) ;
2020-12-23 19:02:01 +00:00
} catch ( e ) {
if ( maxTries === 1 ) {
2021-09-21 15:48:09 +00:00
logger . log ( "Failed to check for protocol support and no retries remain: assuming no support" , e ) ;
2020-12-23 19:02:01 +00:00
} else {
2021-09-21 15:48:09 +00:00
logger . log ( "Failed to check for protocol support: will retry" , e ) ;
2022-11-30 11:32:56 +00:00
window . setTimeout ( ( ) = > {
2021-02-12 20:55:54 +00:00
this . checkProtocols ( maxTries - 1 ) ;
2020-12-23 19:02:01 +00:00
} , 10000 ) ;
}
}
}
2022-03-02 09:59:01 +00:00
private shouldObeyAssertedfIdentity ( ) : boolean {
2023-02-16 17:21:44 +00:00
return ! ! SdkConfig . getObject ( "voip" ) ? . get ( "obey_asserted_identity" ) ;
2022-03-02 09:59:01 +00:00
}
2023-02-16 17:21:44 +00:00
public getSupportsPstnProtocol ( ) : boolean | null {
2021-02-12 20:55:54 +00:00
return this . supportsPstnProtocol ;
}
2023-02-16 17:21:44 +00:00
public getSupportsVirtualRooms ( ) : boolean | null {
2021-06-02 16:47:29 +00:00
return this . supportsSipNativeVirtual ;
2020-12-23 19:02:01 +00:00
}
2022-07-01 12:43:42 +00:00
public async pstnLookup ( phoneNumber : string ) : Promise < ThirdpartyLookupResponse [ ] > {
try {
2023-06-21 16:29:44 +00:00
return await MatrixClientPeg . safeGet ( ) . getThirdpartyUser (
2022-07-01 12:43:42 +00:00
this . pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN ,
{
"m.id.phone" : phoneNumber ,
} ,
) ;
} catch ( e ) {
logger . warn ( "Failed to lookup user from phone number" , e ) ;
return Promise . resolve ( [ ] ) ;
}
2021-02-12 20:55:54 +00:00
}
2022-07-01 12:43:42 +00:00
public async sipVirtualLookup ( nativeMxid : string ) : Promise < ThirdpartyLookupResponse [ ] > {
try {
2023-06-21 16:29:44 +00:00
return await MatrixClientPeg . safeGet ( ) . getThirdpartyUser ( PROTOCOL_SIP_VIRTUAL , {
2022-07-01 12:43:42 +00:00
native_mxid : nativeMxid ,
} ) ;
} catch ( e ) {
logger . warn ( "Failed to query SIP identity for user" , e ) ;
return Promise . resolve ( [ ] ) ;
}
2021-02-12 20:55:54 +00:00
}
2022-07-01 12:43:42 +00:00
public async sipNativeLookup ( virtualMxid : string ) : Promise < ThirdpartyLookupResponse [ ] > {
try {
2023-06-21 16:29:44 +00:00
return await MatrixClientPeg . safeGet ( ) . getThirdpartyUser ( PROTOCOL_SIP_NATIVE , {
2022-07-01 12:43:42 +00:00
virtual_mxid : virtualMxid ,
} ) ;
} catch ( e ) {
logger . warn ( "Failed to query identity for SIP user" , e ) ;
return Promise . resolve ( [ ] ) ;
}
2021-02-12 20:55:54 +00:00
}
2021-11-30 18:09:13 +00:00
private onCallIncoming = ( call : MatrixCall ) : void = > {
// if the runtime env doesn't do VoIP, stop here.
2023-06-21 16:29:44 +00:00
if ( ! MatrixClientPeg . get ( ) ? . supportsVoip ( ) ) {
2021-11-30 18:09:13 +00:00
return ;
}
2022-08-30 19:13:39 +00:00
const mappedRoomId = LegacyCallHandler . instance . roomIdForCall ( call ) ;
2023-04-20 08:49:10 +00:00
if ( ! mappedRoomId ) return ;
2021-11-30 18:09:13 +00:00
if ( this . getCallForRoom ( mappedRoomId ) ) {
logger . log (
"Got incoming call for room " + mappedRoomId + " but there's already a call for this room: ignoring" ,
) ;
return ;
}
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.
2023-06-21 16:29:44 +00:00
const cli = MatrixClientPeg . safeGet ( ) ;
2023-04-20 08:49:10 +00:00
const room = cli . getRoom ( call . roomId ) ;
if ( room ) cli . prepareToEncrypt ( room ) ;
2021-06-29 12:11:58 +00:00
} ;
2020-09-24 15:16:20 +00:00
2023-02-16 17:21:44 +00:00
public getCallById ( callId : string ) : MatrixCall | null {
2021-05-30 15:00:33 +00:00
for ( const call of this . calls . values ( ) ) {
if ( call . callId === callId ) return call ;
}
2021-06-04 05:42:17 +00:00
return null ;
2021-05-30 15:00:33 +00:00
}
2021-11-30 18:09:13 +00:00
public getCallForRoom ( roomId : string ) : MatrixCall | null {
2020-09-24 17:18:26 +00:00
return this . calls . get ( roomId ) || null ;
2020-09-24 15:16:20 +00:00
}
2021-11-30 18:09:13 +00:00
public getAllActiveCalls ( ) : MatrixCall [ ] {
2023-02-16 17:21:44 +00:00
const activeCalls : MatrixCall [ ] = [ ] ;
2020-12-03 17:45:49 +00:00
for ( const call of this . calls . values ( ) ) {
if ( call . state !== CallState . Ended && call . state !== CallState . Ringing ) {
activeCalls . push ( call ) ;
}
}
return activeCalls ;
}
2021-11-30 18:09:13 +00:00
public getAllActiveCallsNotInRoom ( notInThisRoomId : string ) : MatrixCall [ ] {
2023-02-16 17:21:44 +00:00
const callsNotInThatRoom : MatrixCall [ ] = [ ] ;
2020-12-03 17:45:49 +00:00
for ( const [ roomId , call ] of this . calls . entries ( ) ) {
if ( roomId !== notInThisRoomId && call . state !== CallState . Ended ) {
callsNotInThatRoom . push ( call ) ;
}
}
return callsNotInThatRoom ;
}
2023-01-12 13:25:14 +00:00
public getAllActiveCallsForPip ( roomId : string ) : MatrixCall [ ] {
2023-06-21 16:29:44 +00:00
const room = MatrixClientPeg . safeGet ( ) . getRoom ( roomId ) ;
2023-03-08 11:48:58 +00:00
if ( room && WidgetLayoutStore . instance . hasMaximisedWidget ( room ) ) {
2021-11-25 21:14:19 +00:00
// This checks if there is space for the call view in the aux panel
// If there is no space any call should be displayed in PiP
return this . getAllActiveCalls ( ) ;
}
return this . getAllActiveCallsNotInRoom ( roomId ) ;
}
2023-02-13 11:39:16 +00:00
public getTransfereeForCallId ( callId : string ) : MatrixCall | undefined {
return this . transferees . get ( callId ) ;
2021-03-25 19:56:21 +00:00
}
2021-11-30 18:09:13 +00:00
public play ( audioId : AudioID ) : void {
2022-08-30 19:13:39 +00:00
const logPrefix = ` LegacyCallHandler.play( ${ audioId } ): ` ;
2022-06-06 18:47:40 +00:00
logger . debug ( ` ${ logPrefix } beginning of function ` ) ;
2020-09-24 15:16:20 +00:00
// TODO: Attach an invisible element for this instead
// which listens?
const audio = document . getElementById ( audioId ) as HTMLMediaElement ;
if ( audio ) {
2022-12-01 04:08:09 +00:00
this . addEventListenersForAudioElement ( audio ) ;
2023-01-12 13:25:14 +00:00
const playAudio = async ( ) : Promise < void > = > {
2020-09-24 15:16:20 +00:00
try {
2022-12-01 04:08:09 +00:00
if ( audio . muted ) {
logger . error (
` ${ logPrefix } <audio> element was unexpectedly muted but we recovered ` +
` gracefully by unmuting it ` ,
) ;
// Recover gracefully
audio . muted = false ;
}
2020-09-24 15:16:20 +00:00
// This still causes the chrome debugger to break on promise rejection if
// the promise is rejected, even though we're catching the exception.
2022-12-01 04:08:09 +00:00
logger . debug ( ` ${ logPrefix } attempting to play audio at volume= ${ audio . volume } ` ) ;
2020-09-24 15:16:20 +00:00
await audio . play ( ) ;
2022-06-06 18:47:40 +00:00
logger . debug ( ` ${ logPrefix } playing audio successfully ` ) ;
2020-09-24 15:16:20 +00:00
} catch ( e ) {
// This is usually because the user hasn't interacted with the document,
// or chrome doesn't think so and is denying the request. Not sure what
// we can really do here...
// https://github.com/vector-im/element-web/issues/7657
2022-06-06 18:47:40 +00:00
logger . warn ( ` ${ logPrefix } unable to play audio clip ` , e ) ;
2020-09-24 15:16:20 +00:00
}
} ;
2020-09-24 17:28:46 +00:00
if ( this . audioPromises . has ( audioId ) ) {
this . audioPromises . set (
audioId ,
2023-04-20 08:49:10 +00:00
this . audioPromises . get ( audioId ) ! . then ( ( ) = > {
2020-09-24 15:16:20 +00:00
audio . load ( ) ;
return playAudio ( ) ;
2020-09-24 17:28:46 +00:00
} ) ,
) ;
2020-09-24 15:16:20 +00:00
} else {
2020-09-24 17:28:46 +00:00
this . audioPromises . set ( audioId , playAudio ( ) ) ;
2020-09-24 15:16:20 +00:00
}
2022-06-06 18:47:40 +00:00
} else {
logger . warn ( ` ${ logPrefix } unable to find <audio> element for ${ audioId } ` ) ;
2020-09-24 15:16:20 +00:00
}
}
2021-11-30 18:09:13 +00:00
public pause ( audioId : AudioID ) : void {
2022-08-30 19:13:39 +00:00
const logPrefix = ` LegacyCallHandler.pause( ${ audioId } ): ` ;
2022-06-06 18:47:40 +00:00
logger . debug ( ` ${ logPrefix } beginning of function ` ) ;
2020-09-24 15:16:20 +00:00
// TODO: Attach an invisible element for this instead
// which listens?
const audio = document . getElementById ( audioId ) as HTMLMediaElement ;
2023-01-12 13:25:14 +00:00
const pauseAudio = ( ) : void = > {
2022-06-06 18:47:40 +00:00
logger . debug ( ` ${ logPrefix } pausing audio ` ) ;
// pause doesn't return a promise, so just do it
audio . pause ( ) ;
} ;
2020-09-24 15:16:20 +00:00
if ( audio ) {
2020-09-24 17:28:46 +00:00
if ( this . audioPromises . has ( audioId ) ) {
2023-04-20 08:49:10 +00:00
this . audioPromises . set ( audioId , this . audioPromises . get ( audioId ) ! . then ( pauseAudio ) ) ;
2020-09-24 15:16:20 +00:00
} else {
2022-06-06 18:47:40 +00:00
pauseAudio ( ) ;
2020-09-24 15:16:20 +00:00
}
2022-06-06 18:47:40 +00:00
} else {
logger . warn ( ` ${ logPrefix } unable to find <audio> element for ${ audioId } ` ) ;
2020-09-24 15:16:20 +00:00
}
}
2021-11-30 18:09:13 +00:00
private matchesCallForThisRoom ( call : MatrixCall ) : boolean {
2020-10-13 14:08:23 +00:00
// 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.
2021-04-23 13:39:39 +00:00
const mappedRoomId = this . roomIdForCall ( call ) ;
2021-01-21 19:20:35 +00:00
2023-04-20 08:49:10 +00:00
const callForThisRoom = mappedRoomId ? this . getCallForRoom ( mappedRoomId ) : null ;
2023-02-16 17:21:44 +00:00
return ! ! callForThisRoom && call . callId === callForThisRoom . callId ;
2020-10-13 14:08:23 +00:00
}
2021-11-30 18:09:13 +00:00
private setCallListeners ( call : MatrixCall ) : void {
2021-07-15 08:55:58 +00:00
let mappedRoomId = this . roomIdForCall ( call ) ;
2021-01-21 19:20:35 +00:00
2020-11-27 12:53:09 +00:00
call . on ( CallEvent . Error , ( err : CallError ) = > {
2020-10-13 14:08:23 +00:00
if ( ! this . matchesCallForThisRoom ( call ) ) return ;
2021-10-15 14:30:53 +00:00
logger . error ( "Call error:" , err ) ;
2020-11-27 12:53:09 +00:00
if ( err . code === CallErrorCode . NoUserMedia ) {
this . showMediaCaptureError ( call ) ;
return ;
}
2020-09-24 15:16:20 +00:00
if (
2023-06-21 16:29:44 +00:00
MatrixClientPeg . safeGet ( ) . getTurnServers ( ) . length === 0 &&
2020-09-24 15:16:20 +00:00
SettingsStore . getValue ( "fallbackICEServerAllowed" ) === null
) {
this . showICEFallbackPrompt ( ) ;
return ;
}
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2020-09-24 15:16:20 +00:00
title : _t ( "Call Failed" ) ,
description : err.message ,
} ) ;
} ) ;
2020-10-12 09:25:23 +00:00
call . on ( CallEvent . Hangup , ( ) = > {
2023-04-25 08:28:48 +00:00
if ( ! mappedRoomId || ! this . matchesCallForThisRoom ( call ) ) return ;
2020-10-13 14:08:23 +00:00
2023-05-05 16:08:07 +00:00
if ( isNotNull ( mappedRoomId ) ) {
this . removeCallForRoom ( mappedRoomId ) ;
}
2020-09-24 15:16:20 +00:00
} ) ;
2020-10-12 09:25:23 +00:00
call . on ( CallEvent . State , ( newState : CallState , oldState : CallState ) = > {
2021-08-17 08:23:54 +00:00
this . onCallStateChanged ( newState , oldState , call ) ;
2020-09-24 15:16:20 +00:00
} ) ;
2020-10-13 14:08:23 +00:00
call . on ( CallEvent . Replaced , ( newCall : MatrixCall ) = > {
2023-04-25 08:28:48 +00:00
if ( ! mappedRoomId || ! this . matchesCallForThisRoom ( call ) ) return ;
2020-10-13 14:08:23 +00:00
2021-09-21 15:48:09 +00:00
logger . log ( ` Call ID ${ call . callId } is being replaced by call ID ${ newCall . callId } ` ) ;
2020-10-13 14:08:23 +00:00
if ( call . state === CallState . Ringing ) {
this . pause ( AudioID . Ring ) ;
} else if ( call . state === CallState . InviteSent ) {
this . pause ( AudioID . Ringback ) ;
}
2023-05-05 16:08:07 +00:00
if ( isNotNull ( mappedRoomId ) ) {
this . removeCallForRoom ( mappedRoomId ) ;
this . addCallForRoom ( mappedRoomId , newCall ) ;
}
2021-09-02 17:41:26 +00:00
this . setCallListeners ( newCall ) ;
2020-10-13 14:08:23 +00:00
this . setCallState ( newCall , newCall . state ) ;
} ) ;
2023-01-12 13:25:14 +00:00
call . on ( CallEvent . AssertedIdentityChanged , async ( ) : Promise < void > = > {
2023-04-25 08:28:48 +00:00
if ( ! mappedRoomId || ! this . matchesCallForThisRoom ( call ) ) return ;
2021-04-19 19:30:51 +00:00
2021-09-21 15:48:09 +00:00
logger . log ( ` Call ID ${ call . callId } got new asserted identity: ` , call . getRemoteAssertedIdentity ( ) ) ;
2021-04-19 19:30:51 +00:00
2022-03-02 09:59:01 +00:00
if ( ! this . shouldObeyAssertedfIdentity ( ) ) {
logger . log ( "asserted identity not enabled in config: ignoring" ) ;
return ;
}
2023-02-16 17:21:44 +00:00
const newAssertedIdentity = call . getRemoteAssertedIdentity ( ) ? . id ;
2021-04-19 19:30:51 +00:00
let newNativeAssertedIdentity = newAssertedIdentity ;
if ( newAssertedIdentity ) {
const response = await this . sipNativeLookup ( newAssertedIdentity ) ;
2021-06-02 16:39:13 +00:00
if ( response . length && response [ 0 ] . fields . lookup_success ) {
newNativeAssertedIdentity = response [ 0 ] . userid ;
}
2021-04-19 19:30:51 +00:00
}
2021-09-21 15:48:09 +00:00
logger . log ( ` Asserted identity ${ newAssertedIdentity } mapped to ${ newNativeAssertedIdentity } ` ) ;
2021-04-19 19:30:51 +00:00
if ( newNativeAssertedIdentity ) {
2023-02-13 11:39:16 +00:00
this . assertedIdentityNativeUsers . set ( call . callId , newNativeAssertedIdentity ) ;
2021-04-19 19:30:51 +00:00
2021-04-19 20:05:05 +00:00
// If we don't already have a room with this user, make one. This will be slightly odd
// if they called us because we'll be inviting them, but there's not much we can do about
// this if we want the actual, native room to exist (which we do). This is why it's
// important to only obey asserted identity in trusted environments, since anyone you're
// on a call with can cause you to send a room invite to someone.
2023-06-21 16:29:44 +00:00
await ensureDMExists ( MatrixClientPeg . safeGet ( ) , newNativeAssertedIdentity ) ;
2021-04-19 19:30:51 +00:00
2021-04-23 13:39:39 +00:00
const newMappedRoomId = this . roomIdForCall ( call ) ;
2021-09-21 15:48:09 +00:00
logger . log ( ` Old room ID: ${ mappedRoomId } , new room ID: ${ newMappedRoomId } ` ) ;
2023-05-05 16:08:07 +00:00
if ( newMappedRoomId !== mappedRoomId && isNotNull ( mappedRoomId ) && isNotNull ( newMappedRoomId ) ) {
2021-04-19 19:30:51 +00:00
this . removeCallForRoom ( mappedRoomId ) ;
mappedRoomId = newMappedRoomId ;
2021-09-21 15:48:09 +00:00
logger . log ( "Moving call to room " + mappedRoomId ) ;
2021-09-02 13:41:10 +00:00
this . addCallForRoom ( mappedRoomId , call , true ) ;
2021-04-19 19:30:51 +00:00
}
}
} ) ;
2020-09-24 15:16:20 +00:00
}
2023-02-16 17:21:44 +00:00
private onCallStateChanged = ( newState : CallState , oldState : CallState | null , call : MatrixCall ) : void = > {
2021-08-17 08:23:54 +00:00
const mappedRoomId = this . roomIdForCall ( call ) ;
2023-05-10 07:41:55 +00:00
if ( ! mappedRoomId || ! this . matchesCallForThisRoom ( call ) ) return ;
2021-08-17 08:23:54 +00:00
this . setCallState ( call , newState ) ;
2021-11-30 18:09:13 +00:00
dis . dispatch ( {
action : "call_state" ,
room_id : mappedRoomId ,
state : newState ,
} ) ;
2021-08-17 08:23:54 +00:00
switch ( oldState ) {
case CallState . Ringing :
this . pause ( AudioID . Ring ) ;
break ;
case CallState . InviteSent :
this . pause ( AudioID . Ringback ) ;
break ;
}
if ( newState !== CallState . Ringing ) {
this . silencedCalls . delete ( call . callId ) ;
}
switch ( newState ) {
case CallState . Ringing : {
2023-06-21 16:29:44 +00:00
const incomingCallPushRule = new PushProcessor ( MatrixClientPeg . safeGet ( ) ) . getPushRuleById (
2021-08-17 08:23:54 +00:00
RuleId . IncomingCall ,
) ;
const pushRuleEnabled = incomingCallPushRule ? . enabled ;
2023-05-05 16:08:07 +00:00
// actions can be either Tweaks | PushRuleActionName, ie an object or a string type enum
// and we want to only run this check on the Tweaks
2021-08-17 08:23:54 +00:00
const tweakSetToRing = incomingCallPushRule ? . actions . some (
2023-05-05 16:08:07 +00:00
( action ) = >
typeof action !== "string" && action . set_tweak === TweakName . Sound && action . value === "ring" ,
2021-08-17 08:23:54 +00:00
) ;
2022-10-17 09:16:04 +00:00
if ( pushRuleEnabled && tweakSetToRing && ! this . isForcedSilent ( ) ) {
2021-08-17 08:23:54 +00:00
this . play ( AudioID . Ring ) ;
} else {
this . silenceCall ( call . callId ) ;
}
break ;
}
case CallState . InviteSent : {
this . play ( AudioID . Ringback ) ;
break ;
}
case CallState . Ended : {
const hangupReason = call . hangupReason ;
2023-05-05 16:08:07 +00:00
if ( isNotNull ( mappedRoomId ) ) {
this . removeCallForRoom ( mappedRoomId ) ;
}
2021-08-17 08:23:54 +00:00
if ( oldState === CallState . InviteSent && call . hangupParty === CallParty . Remote ) {
this . play ( AudioID . Busy ) ;
// Don't show a modal when we got rejected/the call was hung up
if ( ! hangupReason || [ CallErrorCode . UserHangup , "user hangup" ] . includes ( hangupReason ) ) break ;
2023-05-09 17:24:40 +00:00
let title : string ;
let description : string ;
2021-08-17 08:23:54 +00:00
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
if ( call . hangupReason === CallErrorCode . UserBusy ) {
title = _t ( "User Busy" ) ;
description = _t ( "The user you called is busy." ) ;
} else {
title = _t ( "Call Failed" ) ;
description = _t ( "The call could not be established" ) ;
}
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-08-17 08:23:54 +00:00
title ,
description ,
} ) ;
} else if ( hangupReason === CallErrorCode . AnsweredElsewhere && oldState === CallState . Connecting ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-08-17 08:23:54 +00:00
title : _t ( "Answered Elsewhere" ) ,
description : _t ( "The call was answered on another device." ) ,
} ) ;
} else if ( oldState !== CallState . Fledgling && oldState !== CallState . Ringing ) {
// don't play the end-call sound for calls that never got off the ground
this . play ( AudioID . CallEnd ) ;
}
2023-05-05 16:08:07 +00:00
if ( isNotNull ( mappedRoomId ) ) {
this . logCallStats ( call , mappedRoomId ) ;
}
2021-08-17 08:23:54 +00:00
break ;
}
}
} ;
2021-11-30 18:09:13 +00:00
private async logCallStats ( call : MatrixCall , mappedRoomId : string ) : Promise < void > {
2021-01-26 09:41:57 +00:00
const stats = await call . getCurrentCallStats ( ) ;
logger . debug (
` Call completed. Call ID: ${ call . callId } , virtual room ID: ${ call . roomId } , ` +
` user-facing room ID: ${ mappedRoomId } , direction: ${ call . direction } , ` +
` our Party ID: ${ call . ourPartyId } , hangup party: ${ call . hangupParty } , ` +
` hangup reason: ${ call . hangupReason } ` ,
) ;
2021-02-09 13:52:48 +00:00
if ( ! stats ) {
logger . debug (
2023-02-27 09:15:27 +00:00
"Call statistics are undefined. The call has probably failed before a peerConn was established" ,
2021-02-09 13:52:48 +00:00
) ;
return ;
}
2021-01-26 09:41:57 +00:00
logger . debug ( "Local candidates:" ) ;
for ( const cand of stats . filter ( ( item ) = > item . type === "local-candidate" ) ) {
2021-01-26 10:52:35 +00:00
const address = cand . address || cand . ip ; // firefox uses 'address', chrome uses 'ip'
2021-01-26 09:41:57 +00:00
logger . debug (
2021-01-26 10:52:35 +00:00
` ${ cand . id } - type: ${ cand . candidateType } , address: ${ address } , port: ${ cand . port } , ` +
2021-01-26 09:41:57 +00:00
` protocol: ${ cand . protocol } , relay protocol: ${ cand . relayProtocol } , network type: ${ cand . networkType } ` ,
) ;
}
logger . debug ( "Remote candidates:" ) ;
for ( const cand of stats . filter ( ( item ) = > item . type === "remote-candidate" ) ) {
2021-01-26 10:52:35 +00:00
const address = cand . address || cand . ip ; // firefox uses 'address', chrome uses 'ip'
2021-01-26 09:41:57 +00:00
logger . debug (
2021-01-26 10:52:35 +00:00
` ${ cand . id } - type: ${ cand . candidateType } , address: ${ address } , port: ${ cand . port } , ` +
2021-01-26 09:41:57 +00:00
` protocol: ${ cand . protocol } ` ,
) ;
}
logger . debug ( "Candidate pairs:" ) ;
for ( const pair of stats . filter ( ( item ) = > item . type === "candidate-pair" ) ) {
logger . debug (
` ${ pair . localCandidateId } / ${ pair . remoteCandidateId } - state: ${ pair . state } , ` +
` nominated: ${ pair . nominated } , ` +
` requests sent ${ pair . requestsSent } , requests received ${ pair . requestsReceived } , ` +
` responses received: ${ pair . responsesReceived } , responses sent: ${ pair . responsesSent } , ` +
` bytes received: ${ pair . bytesReceived } , bytes sent: ${ pair . bytesSent } , ` ,
) ;
}
2022-02-04 14:02:56 +00:00
logger . debug ( "Outbound RTP:" ) ;
for ( const s of stats . filter ( ( item ) = > item . type === "outbound-rtp" ) ) {
logger . debug ( s ) ;
}
logger . debug ( "Inbound RTP:" ) ;
for ( const s of stats . filter ( ( item ) = > item . type === "inbound-rtp" ) ) {
logger . debug ( s ) ;
}
2021-01-26 09:41:57 +00:00
}
2021-11-30 18:09:13 +00:00
private setCallState ( call : MatrixCall , status : CallState ) : void {
2022-08-30 19:13:39 +00:00
const mappedRoomId = LegacyCallHandler . instance . roomIdForCall ( call ) ;
2021-01-21 19:20:35 +00:00
logger . log ( ` Call state in ${ mappedRoomId } changed to ${ status } ` ) ;
2020-09-24 15:16:20 +00:00
2022-08-30 19:13:39 +00:00
const toastKey = getIncomingLegacyCallToastKey ( call . callId ) ;
2021-07-24 11:05:14 +00:00
if ( status === CallState . Ringing ) {
ToastStore . sharedInstance ( ) . addOrReplaceToast ( {
key : toastKey ,
priority : 100 ,
2022-08-30 19:13:39 +00:00
component : IncomingLegacyCallToast ,
bodyClassName : "mx_IncomingLegacyCallToast" ,
2021-07-24 11:05:14 +00:00
props : { call } ,
} ) ;
} else {
ToastStore . sharedInstance ( ) . dismissToast ( toastKey ) ;
}
2022-08-30 19:13:39 +00:00
this . emit ( LegacyCallHandlerEvent . CallState , mappedRoomId , status ) ;
2020-09-24 15:16:20 +00:00
}
2021-11-30 18:09:13 +00:00
private removeCallForRoom ( roomId : string ) : void {
2021-09-21 15:48:09 +00:00
logger . log ( "Removing call for room " , roomId ) ;
2020-10-09 17:56:07 +00:00
this . calls . delete ( roomId ) ;
2022-08-30 19:13:39 +00:00
this . emit ( LegacyCallHandlerEvent . CallsChanged , this . calls ) ;
2020-10-01 10:28:42 +00:00
}
2021-11-30 18:09:13 +00:00
private showICEFallbackPrompt ( ) : void {
2023-06-21 16:29:44 +00:00
const cli = MatrixClientPeg . safeGet ( ) ;
2022-06-14 16:51:51 +00:00
Modal . createDialog (
QuestionDialog ,
{
2020-09-24 15:16:20 +00:00
title : _t ( "Call failed due to misconfigured server" ) ,
description : (
< div >
2021-07-19 21:43:11 +00:00
< p >
{ _t (
2023-08-22 15:32:05 +00:00
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably." ,
2020-09-24 15:16:20 +00:00
{ homeserverDomain : cli.getDomain ( ) } ,
2023-06-05 13:53:11 +00:00
{ code : ( sub : string ) = > < code > { sub } < / code > } ,
2021-07-19 21:43:11 +00:00
) }
< / p >
< p >
{ _t (
2023-08-22 15:32:05 +00:00
"Alternatively, you can try to use the public server at <server/>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings." ,
2023-02-16 17:21:44 +00:00
undefined ,
2023-06-05 13:53:11 +00:00
{ server : ( ) = > < code > { new URL ( FALLBACK_ICE_SERVER ) . pathname } < / code > } ,
2021-07-19 21:43:11 +00:00
) }
< / p >
2020-09-24 15:16:20 +00:00
< / div >
) ,
2023-06-05 13:53:11 +00:00
button : _t ( "Try using %(server)s" , {
server : new URL ( FALLBACK_ICE_SERVER ) . pathname ,
} ) ,
2023-08-22 19:55:15 +00:00
cancelButton : _t ( "action|ok" ) ,
2020-09-24 15:16:20 +00:00
onFinished : ( allow ) = > {
SettingsStore . setValue ( "fallbackICEServerAllowed" , null , SettingLevel . DEVICE , allow ) ;
2023-04-20 08:49:10 +00:00
cli . setFallbackICEServerAllowed ( ! ! allow ) ;
2022-12-12 11:24:14 +00:00
} ,
2020-09-24 15:16:20 +00:00
} ,
2023-02-16 17:21:44 +00:00
undefined ,
2020-09-24 15:16:20 +00:00
true ,
) ;
}
2021-11-30 18:09:13 +00:00
private showMediaCaptureError ( call : MatrixCall ) : void {
2020-11-27 12:53:09 +00:00
let title ;
let description ;
if ( call . type === CallType . Voice ) {
title = _t ( "Unable to access microphone" ) ;
description = (
< div >
2021-07-19 21:43:11 +00:00
{ _t (
2023-08-22 15:32:05 +00:00
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly." ,
2021-07-19 21:43:11 +00:00
) }
2020-11-27 12:53:09 +00:00
< / div >
) ;
} else if ( call . type === CallType . Video ) {
title = _t ( "Unable to access webcam / microphone" ) ;
description = (
< div >
2021-07-19 21:43:11 +00:00
{ _t ( "Call failed because webcam or microphone could not be accessed. Check that:" ) }
2020-11-27 12:53:09 +00:00
< ul >
2021-07-19 21:43:11 +00:00
< li > { _t ( "A microphone and webcam are plugged in and set up correctly" ) } < / li >
< li > { _t ( "Permission is granted to use the webcam" ) } < / li >
< li > { _t ( "No other application is using the webcam" ) } < / li >
2020-11-27 12:53:09 +00:00
< / ul >
< / div >
) ;
}
2022-06-14 16:51:51 +00:00
Modal . createDialog (
ErrorDialog ,
{
2020-11-27 12:53:09 +00:00
title ,
description ,
} ,
2023-02-16 17:21:44 +00:00
undefined ,
2020-11-27 12:53:09 +00:00
true ,
) ;
}
2020-10-09 17:56:07 +00:00
2021-11-30 18:09:13 +00:00
private async placeMatrixCall ( roomId : string , type : CallType , transferee? : MatrixCall ) : Promise < void > {
2023-06-21 16:29:44 +00:00
const cli = MatrixClientPeg . safeGet ( ) ;
2021-02-12 20:55:54 +00:00
const mappedRoomId = ( await VoipUserMapper . sharedInstance ( ) . getOrCreateVirtualRoomForRoom ( roomId ) ) || roomId ;
2021-01-21 19:20:35 +00:00
logger . debug ( "Mapped real room " + roomId + " to room ID " + mappedRoomId ) ;
2022-01-20 09:32:15 +00:00
// If we're using a virtual room nd there are any events pending, try to resend them,
// otherwise the call will fail and because its a virtual room, the user won't be able
// to see it to either retry or clear the pending events. There will only be call events
// in this queue, and since we're about to place a new call, they can only be events from
// previous calls that are probably stale by now, so just cancel them.
if ( mappedRoomId !== roomId ) {
2023-06-21 16:29:44 +00:00
const mappedRoom = cli . getRoom ( mappedRoomId ) ;
2023-04-20 08:49:10 +00:00
if ( mappedRoom ? . getPendingEvents ( ) . length ) {
2022-01-20 09:32:15 +00:00
Resend . cancelUnsentEvents ( mappedRoom ) ;
}
}
2023-06-21 16:29:44 +00:00
const timeUntilTurnCresExpire = cli . getTurnServersExpiry ( ) - Date . now ( ) ;
2021-09-21 15:48:09 +00:00
logger . log ( "Current turn creds expire in " + timeUntilTurnCresExpire + " ms" ) ;
2023-06-21 16:29:44 +00:00
const call = cli . createCall ( mappedRoomId ) ! ;
2021-01-21 19:20:35 +00:00
2021-09-02 14:51:44 +00:00
try {
this . addCallForRoom ( roomId , call ) ;
} catch ( e ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-09-02 14:51:44 +00:00
title : _t ( "Already in call" ) ,
description : _t ( "You're already in a call with this person." ) ,
} ) ;
return ;
}
2021-03-25 19:56:21 +00:00
if ( transferee ) {
2023-02-13 11:39:16 +00:00
this . transferees . set ( call . callId , transferee ) ;
2021-03-25 19:56:21 +00:00
}
2021-01-21 19:20:35 +00:00
2020-10-09 17:56:07 +00:00
this . setCallListeners ( call ) ;
2020-10-29 17:56:24 +00:00
2020-12-03 17:45:49 +00:00
this . setActiveCallRoomId ( roomId ) ;
2021-11-30 18:09:13 +00:00
if ( type === CallType . Voice ) {
2020-10-09 17:56:07 +00:00
call . placeVoiceCall ( ) ;
} else if ( type === "video" ) {
2021-03-07 07:13:35 +00:00
call . placeVideoCall ( ) ;
2020-10-09 17:56:07 +00:00
} else {
2021-10-15 14:30:53 +00:00
logger . error ( "Unknown conf call type: " + type ) ;
2020-09-24 15:16:20 +00:00
}
2020-10-09 17:56:07 +00:00
}
2020-09-24 15:16:20 +00:00
2023-04-20 08:49:10 +00:00
public async placeCall ( roomId : string , type : CallType , transferee? : MatrixCall ) : Promise < void > {
2023-06-21 16:29:44 +00:00
const cli = MatrixClientPeg . safeGet ( ) ;
2022-12-19 08:44:19 +00:00
// Pause current broadcast, if any
SdkContextClass . instance . voiceBroadcastPlaybacksStore . getCurrent ( ) ? . pause ( ) ;
if ( SdkContextClass . instance . voiceBroadcastRecordingsStore . getCurrent ( ) ) {
// Do not start a call, if recording a broadcast
showCantStartACallDialog ( ) ;
return ;
}
2021-11-30 18:09:13 +00:00
// We might be using managed hybrid widgets
2023-06-06 14:07:51 +00:00
if ( isManagedHybridWidgetEnabled ( roomId ) ) {
2022-09-25 14:57:25 +00:00
await addManagedHybridWidget ( roomId ) ;
2021-11-30 18:09:13 +00:00
return ;
}
2021-01-29 14:26:33 +00:00
2021-11-30 18:09:13 +00:00
// if the runtime env doesn't do VoIP, whine.
2023-06-21 16:29:44 +00:00
if ( ! cli . supportsVoip ( ) ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-12-06 08:07:02 +00:00
title : _t ( "Calls are unsupported" ) ,
description : _t ( "You cannot place calls in this browser." ) ,
} ) ;
return ;
}
2023-06-21 16:29:44 +00:00
if ( cli . getSyncState ( ) === SyncState . Error ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-12-06 08:07:02 +00:00
title : _t ( "Connectivity to the server has been lost" ) ,
description : _t ( "You cannot place calls without a connection to the server." ) ,
2021-11-30 18:09:13 +00:00
} ) ;
return ;
}
2020-09-24 15:16:20 +00:00
2021-11-30 18:09:13 +00:00
// don't allow > 2 calls to be placed.
if ( this . getAllActiveCalls ( ) . length > 1 ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-11-30 18:09:13 +00:00
title : _t ( "Too Many Calls" ) ,
description : _t ( "You've reached the maximum number of simultaneous calls." ) ,
} ) ;
return ;
}
2020-12-03 17:45:49 +00:00
2023-06-21 16:29:44 +00:00
const room = cli . getRoom ( roomId ) ;
2021-11-30 18:09:13 +00:00
if ( ! room ) {
logger . error ( ` Room ${ roomId } does not exist. ` ) ;
return ;
}
2020-09-24 15:16:20 +00:00
2021-11-30 18:09:13 +00:00
// We leave the check for whether there's already a call in this room until later,
// otherwise it can race.
2020-09-24 15:16:20 +00:00
2022-08-01 17:28:33 +00:00
const members = getJoinedNonFunctionalMembers ( room ) ;
2022-01-26 13:31:00 +00:00
if ( members . length <= 1 ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-11-30 18:09:13 +00:00
description : _t ( "You cannot place a call with yourself." ) ,
} ) ;
2022-01-26 13:31:00 +00:00
} else if ( members . length === 2 ) {
2021-11-30 18:09:13 +00:00
logger . info ( ` Place ${ type } call in ${ roomId } ` ) ;
2020-12-03 17:45:49 +00:00
2022-09-25 14:57:25 +00:00
await this . placeMatrixCall ( roomId , type , transferee ) ;
2021-11-30 18:09:13 +00:00
} else {
// > 2
2022-09-25 14:57:25 +00:00
await this . placeJitsiCall ( roomId , type ) ;
2021-11-30 18:09:13 +00:00
}
}
2020-12-03 17:45:49 +00:00
2021-11-30 18:09:13 +00:00
public hangupAllCalls ( ) : void {
for ( const call of this . calls . values ( ) ) {
this . stopRingingIfPossible ( call . callId ) ;
call . hangup ( CallErrorCode . UserHangup , false ) ;
}
}
2021-09-02 13:41:10 +00:00
2021-11-30 18:09:13 +00:00
public hangupOrReject ( roomId : string , reject? : boolean ) : void {
const call = this . calls . get ( roomId ) ;
2021-02-16 14:52:11 +00:00
2021-11-30 18:09:13 +00:00
// no call to hangup
if ( ! call ) return ;
2021-08-26 13:00:56 +00:00
2021-11-30 18:09:13 +00:00
this . stopRingingIfPossible ( call . callId ) ;
2021-08-26 13:00:56 +00:00
2021-11-30 18:09:13 +00:00
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)
}
2021-08-26 13:00:56 +00:00
2021-11-30 18:09:13 +00:00
public answerCall ( roomId : string ) : void {
// no call to answer
if ( ! this . calls . has ( roomId ) ) return ;
2023-02-24 15:28:40 +00:00
const call = this . calls . get ( roomId ) ! ;
this . stopRingingIfPossible ( call . callId ) ;
2021-11-30 18:09:13 +00:00
if ( this . getAllActiveCalls ( ) . length > 1 ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-11-30 18:09:13 +00:00
title : _t ( "Too Many Calls" ) ,
description : _t ( "You've reached the maximum number of simultaneous calls." ) ,
} ) ;
return ;
2021-06-02 16:39:13 +00:00
}
2021-11-30 18:09:13 +00:00
call . answer ( ) ;
this . setActiveCallRoomId ( roomId ) ;
2022-02-10 14:29:55 +00:00
dis . dispatch < ViewRoomPayload > ( {
2021-11-30 18:09:13 +00:00
action : Action.ViewRoom ,
room_id : roomId ,
2022-02-17 18:03:27 +00:00
metricsTrigger : "WebAcceptCall" ,
2021-11-30 18:09:13 +00:00
} ) ;
}
2021-06-02 16:39:13 +00:00
2021-08-26 13:00:36 +00:00
private stopRingingIfPossible ( callId : string ) : void {
this . silencedCalls . delete ( callId ) ;
if ( this . areAnyCallsUnsilenced ( ) ) return ;
this . pause ( AudioID . Ring ) ;
}
2022-02-21 12:17:09 +00:00
public async dialNumber ( number : string , transferee? : MatrixCall ) : Promise < void > {
2021-06-02 16:39:13 +00:00
const results = await this . pstnLookup ( number ) ;
if ( ! results || results . length === 0 || ! results [ 0 ] . userid ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-06-02 16:39:13 +00:00
title : _t ( "Unable to look up phone number" ) ,
description : _t ( "There was an error looking up the phone number" ) ,
} ) ;
return ;
2020-09-24 15:16:20 +00:00
}
2021-06-02 16:39:13 +00:00
const userId = results [ 0 ] . userid ;
// Now check to see if this is a virtual user, in which case we should find the
// native user
2021-06-02 16:47:29 +00:00
let nativeUserId ;
if ( this . getSupportsVirtualRooms ( ) ) {
const nativeLookupResults = await this . sipNativeLookup ( userId ) ;
const lookupSuccess = nativeLookupResults . length > 0 && nativeLookupResults [ 0 ] . fields . lookup_success ;
nativeUserId = lookupSuccess ? nativeLookupResults [ 0 ] . userid : userId ;
2021-09-21 15:48:09 +00:00
logger . log ( "Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId ) ;
2021-06-02 16:47:29 +00:00
} else {
nativeUserId = userId ;
2020-09-24 15:16:20 +00:00
}
2021-06-02 16:39:13 +00:00
2023-06-21 16:29:44 +00:00
const roomId = await ensureDMExists ( MatrixClientPeg . safeGet ( ) , nativeUserId ) ;
2023-05-09 17:24:40 +00:00
if ( ! roomId ) {
throw new Error ( "Failed to ensure DM exists for dialing number" ) ;
}
2021-06-02 16:39:13 +00:00
2023-05-09 17:24:40 +00:00
dis . dispatch < ViewRoomPayload > ( {
action : Action.ViewRoom ,
room_id : roomId ,
metricsTrigger : "WebDialPad" ,
} ) ;
2021-08-04 08:46:39 +00:00
2023-05-09 17:24:40 +00:00
await this . placeMatrixCall ( roomId , CallType . Voice , transferee ) ;
2020-09-24 15:16:20 +00:00
}
2021-11-30 18:09:13 +00:00
public async startTransferToPhoneNumber (
call : MatrixCall ,
destination : string ,
consultFirst : boolean ,
) : Promise < void > {
2022-02-21 12:17:09 +00:00
if ( consultFirst ) {
// if we're consulting, we just start by placing a call to the transfer
2022-05-09 22:52:05 +00:00
// target (passing the transferee so the actual transfer can happen later)
2022-02-21 12:17:09 +00:00
this . dialNumber ( destination , call ) ;
return ;
}
2021-07-15 08:55:58 +00:00
const results = await this . pstnLookup ( destination ) ;
if ( ! results || results . length === 0 || ! results [ 0 ] . userid ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-07-15 08:55:58 +00:00
title : _t ( "Unable to transfer call" ) ,
description : _t ( "There was an error looking up the phone number" ) ,
} ) ;
return ;
}
await this . startTransferToMatrixID ( call , results [ 0 ] . userid , consultFirst ) ;
}
2021-11-30 18:09:13 +00:00
public async startTransferToMatrixID ( call : MatrixCall , destination : string , consultFirst : boolean ) : Promise < void > {
2021-07-15 08:55:58 +00:00
if ( consultFirst ) {
2023-06-21 16:29:44 +00:00
const dmRoomId = await ensureDMExists ( MatrixClientPeg . safeGet ( ) , destination ) ;
2023-04-25 08:28:48 +00:00
if ( ! dmRoomId ) {
logger . log ( "Failed to transfer call, could not ensure dm exists" ) ;
Modal . createDialog ( ErrorDialog , {
title : _t ( "Transfer Failed" ) ,
description : _t ( "Failed to transfer call" ) ,
} ) ;
return ;
}
2021-07-15 08:55:58 +00:00
2021-11-30 18:09:13 +00:00
this . placeCall ( dmRoomId , call . type , call ) ;
2022-02-10 14:29:55 +00:00
dis . dispatch < ViewRoomPayload > ( {
2021-11-25 20:49:43 +00:00
action : Action.ViewRoom ,
2021-07-15 08:55:58 +00:00
room_id : dmRoomId ,
should_peek : false ,
joining : false ,
2022-02-17 18:03:27 +00:00
metricsTrigger : undefined , // other
2021-07-15 08:55:58 +00:00
} ) ;
} else {
try {
await call . transfer ( destination ) ;
} catch ( e ) {
2021-09-21 15:48:09 +00:00
logger . log ( "Failed to transfer call" , e ) ;
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2021-07-15 08:55:58 +00:00
title : _t ( "Transfer Failed" ) ,
description : _t ( "Failed to transfer call" ) ,
} ) ;
}
}
}
2021-11-30 18:09:13 +00:00
public setActiveCallRoomId ( activeCallRoomId : string ) : void {
2020-12-03 17:45:49 +00:00
logger . info ( "Setting call in room " + activeCallRoomId + " active" ) ;
for ( const [ roomId , call ] of this . calls . entries ( ) ) {
if ( call . state === CallState . Ended ) continue ;
if ( roomId === activeCallRoomId ) {
call . setRemoteOnHold ( false ) ;
} else {
logger . info ( "Holding call in room " + roomId + " because another call is being set active" ) ;
call . setRemoteOnHold ( true ) ;
}
}
}
2020-12-18 19:35:41 +00:00
/ * *
2020-12-21 11:21:41 +00:00
* @returns true if we are currently in any call where we haven ' t put the remote party on hold
2020-12-18 19:35:41 +00:00
* /
2021-11-30 18:09:13 +00:00
public hasAnyUnheldCall ( ) : boolean {
2020-12-18 19:35:41 +00:00
for ( const call of this . calls . values ( ) ) {
if ( call . state === CallState . Ended ) continue ;
if ( ! call . isRemoteOnHold ( ) ) return true ;
}
return false ;
}
2022-03-22 22:14:11 +00:00
private async placeJitsiCall ( roomId : string , type : CallType ) : Promise < void > {
2023-06-21 16:29:44 +00:00
const client = MatrixClientPeg . safeGet ( ) ;
2022-03-22 22:14:11 +00:00
logger . info ( ` Place conference call in ${ roomId } ` ) ;
2021-11-30 18:09:13 +00:00
2022-03-22 22:14:11 +00:00
dis . dispatch ( { action : "appsDrawer" , show : true } ) ;
2020-09-24 15:16:20 +00:00
2022-03-22 22:14:11 +00:00
// Prevent double clicking the call button
const widget = WidgetStore . instance . getApps ( roomId ) . find ( ( app ) = > WidgetType . JITSI . matches ( app . type ) ) ;
if ( widget ) {
// If there already is a Jitsi widget, pin it
2023-05-05 16:08:07 +00:00
const room = client . getRoom ( roomId ) ;
if ( isNotNull ( room ) ) {
WidgetLayoutStore . instance . moveToContainer ( room , widget , Container . Top ) ;
}
2020-09-24 15:16:20 +00:00
return ;
}
2022-03-22 22:14:11 +00:00
try {
2023-05-23 15:24:12 +00:00
await WidgetUtils . addJitsiWidget ( client , roomId , type , "Jitsi" , false ) ;
2021-09-21 15:48:09 +00:00
logger . log ( "Jitsi widget added" ) ;
2022-03-22 22:14:11 +00:00
} catch ( e ) {
2023-05-05 16:08:07 +00:00
if ( e instanceof MatrixError && e . errcode === "M_FORBIDDEN" ) {
2022-06-14 16:51:51 +00:00
Modal . createDialog ( ErrorDialog , {
2020-09-24 15:16:20 +00:00
title : _t ( "Permission Required" ) ,
description : _t ( "You do not have permission to start a conference call in this room" ) ,
} ) ;
}
2021-10-15 14:30:53 +00:00
logger . error ( e ) ;
2022-03-22 22:14:11 +00:00
}
2020-09-24 15:16:20 +00:00
}
2020-09-28 19:53:44 +00:00
2021-11-30 18:09:13 +00:00
public hangupCallApp ( roomId : string ) : void {
logger . info ( "Leaving conference call in " + roomId ) ;
2020-09-28 19:53:44 +00:00
const roomInfo = WidgetStore . instance . getRoom ( roomId ) ;
if ( ! roomInfo ) return ; // "should never happen" clauses go here
const jitsiWidgets = roomInfo . widgets . filter ( ( w ) = > WidgetType . JITSI . matches ( w . type ) ) ;
jitsiWidgets . forEach ( ( w ) = > {
2022-03-15 12:15:26 +00:00
const messaging = WidgetMessagingStore . instance . getMessagingForUid ( WidgetUtils . getWidgetUid ( w ) ) ;
2020-09-28 19:53:44 +00:00
if ( ! messaging ) return ; // more "should never happen" words
2020-10-01 02:09:23 +00:00
messaging . transport . send ( ElementWidgetActions . HangupCall , { } ) ;
2020-09-28 19:53:44 +00:00
} ) ;
}
2021-09-02 13:41:10 +00:00
2022-01-28 17:05:57 +00:00
/ *
* Shows the transfer dialog for a call , signalling to the other end that
* a transfer is about to happen
* /
public showTransferDialog ( call : MatrixCall ) : void {
call . setRemoteOnHold ( true ) ;
2022-04-01 01:38:00 +00:00
dis . dispatch < OpenInviteDialogPayload > ( {
2022-03-24 22:30:53 +00:00
action : Action.OpenInviteDialog ,
2023-02-28 10:31:48 +00:00
kind : InviteKind.CallTransfer ,
2022-03-24 22:30:53 +00:00
call ,
analyticsName : "Transfer Call" ,
className : "mx_InviteDialog_transferWrapper" ,
onFinishedCallback : ( results ) = > {
if ( results . length === 0 || results [ 0 ] === false ) {
call . setRemoteOnHold ( false ) ;
}
} ,
2022-04-01 01:38:00 +00:00
} ) ;
2022-01-28 17:05:57 +00:00
}
2021-09-03 09:38:39 +00:00
private addCallForRoom ( roomId : string , call : MatrixCall , changedRooms = false ) : void {
2021-09-02 13:41:10 +00:00
if ( this . calls . has ( roomId ) ) {
2021-09-21 15:48:09 +00:00
logger . log ( ` Couldn't add call to room ${ roomId } : already have a call for this room ` ) ;
2021-09-02 13:41:10 +00:00
throw new Error ( "Already have a call for room " + roomId ) ;
}
2021-09-21 15:48:09 +00:00
logger . log ( "setting call for room " + roomId ) ;
2021-09-02 13:41:10 +00:00
this . calls . set ( roomId , call ) ;
// Should we always emit CallsChanged too?
if ( changedRooms ) {
2022-08-30 19:13:39 +00:00
this . emit ( LegacyCallHandlerEvent . CallChangeRoom , call ) ;
2021-09-02 13:41:10 +00:00
} else {
2022-08-30 19:13:39 +00:00
this . emit ( LegacyCallHandlerEvent . CallsChanged , this . calls ) ;
2021-09-02 13:41:10 +00:00
}
}
2020-09-24 15:16:20 +00:00
}