2022-08-30 15:13:39 -04:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
2022-08-30 15:13:39 -04:00
Copyright 2022 The Matrix.org Foundation C.I.C.
2025-01-06 11:18:54 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2022-08-30 15:13:39 -04:00
*/
import { logger } from "matrix-js-sdk/src/logger" ;
2026-01-09 12:04:31 +00:00
import { type MatrixRTCSession , MatrixRTCSessionManagerEvents , type Transport } from "matrix-js-sdk/src/matrixrtc" ;
import { MatrixError , type EmptyObject , type Room } from "matrix-js-sdk/src/matrix" ;
2022-08-30 15:13:39 -04:00
import defaultDispatcher from "../dispatcher/dispatcher" ;
import { UPDATE_EVENT } from "./AsyncStore" ;
import { AsyncStoreWithClient } from "./AsyncStoreWithClient" ;
import WidgetStore from "./WidgetStore" ;
import SettingsStore from "../settings/SettingsStore" ;
import { SettingLevel } from "../settings/SettingLevel" ;
import { Call , CallEvent , ConnectionState } from "../models/Call" ;
export enum CallStoreEvent {
// Signals a change in the call associated with a given room
Call = "call" ,
// Signals a change in the active calls
2024-06-17 13:00:41 +02:00
ConnectedCalls = "connected_calls" ,
2026-01-09 13:33:47 +00:00
// Signals a change in the configured RTC transports.
TransportsUpdated = "transports_updated" ,
2022-08-30 15:13:39 -04:00
}
2025-02-04 13:41:34 +00:00
export class CallStore extends AsyncStoreWithClient < EmptyObject > {
2022-08-30 15:13:39 -04:00
private static _instance : CallStore ;
public static get instance () : CallStore {
if ( ! this . _instance ) {
this . _instance = new CallStore ();
2024-10-10 15:08:43 +01:00
this . _instance . start ();
2022-08-30 15:13:39 -04:00
}
return this . _instance ;
}
2026-01-09 12:04:31 +00:00
private readonly configuredMatrixRTCTransports = new Set < Transport >();
2022-08-30 15:13:39 -04:00
private constructor () {
super ( defaultDispatcher );
2023-11-20 12:24:13 +00:00
this . setMaxListeners ( 100 ); // One for each RoomTile
2022-08-30 15:13:39 -04:00
}
2022-09-16 11:12:27 -04:00
protected async onAction () : Promise < void > {
2022-08-30 15:13:39 -04:00
// nothing to do
}
2026-01-09 12:04:31 +00:00
/**
* Fetch transports used by MatrixRTC services, such as Element Call.
* This function is called once during Store startup which means we don't refetch
* transports every time we need to check for Element Call support.
*/
protected async fetchTransports () : Promise < void > {
if ( ! this . matrixClient ) return ;
2026-01-09 13:33:47 +00:00
this . configuredMatrixRTCTransports . clear ();
2026-01-09 12:04:31 +00:00
// Prefer checking the proper endpoint for transports.
try {
const transports = await this . matrixClient . _unstable_getRTCTransports ();
transports . forEach (( t ) => this . configuredMatrixRTCTransports . add ( t ));
} catch ( ex ) {
// Expected, MSC not implemented.
if ( ex instanceof MatrixError === false || ex . errcode !== "M_NOT_FOUND" ) {
logger . warn ( "Unexpected error when trying to fetch RTC transports" , ex );
}
}
// See https://github.com/matrix-org/matrix-spec-proposals/blob/d61969a9a3696b6c54d7987b1643b5bc03670927/proposals/4143-matrix-rtc.md#discovery-of-foci-using-well-knownmatrixclient
// This well-known option has since been removed from the spec but is still widely deployed.
await this . matrixClient . waitForClientWellKnown ();
const foci = this . matrixClient . getClientWellKnown () ? .[ "org.matrix.msc4143.rtc_foci" ];
if ( Array . isArray ( foci )) {
foci . forEach (( foci ) => this . configuredMatrixRTCTransports . add ( foci ));
}
2026-01-09 13:33:47 +00:00
this . emit ( CallStoreEvent . TransportsUpdated );
2026-01-09 12:04:31 +00:00
}
2022-08-30 15:13:39 -04:00
protected async onReady () : Promise < any > {
2023-04-06 11:10:14 +01:00
if ( ! this . matrixClient ) return ;
2026-01-09 12:04:31 +00:00
// Fetch transports, but don't await the result.
void this . fetchTransports ();
2022-08-30 15:13:39 -04:00
// We assume that the calls present in a room are a function of room
2022-11-28 16:37:32 -05:00
// widgets and group calls, so we initialize the room map here and then
2022-08-30 15:13:39 -04:00
// update it whenever those change
for ( const room of this . matrixClient . getRooms ()) {
this . updateRoom ( room );
}
2024-06-17 13:00:41 +02:00
this . matrixClient . matrixRTC . on ( MatrixRTCSessionManagerEvents . SessionStarted , this . onRTCSessionStart );
2022-08-30 15:13:39 -04:00
WidgetStore . instance . on ( UPDATE_EVENT , this . onWidgets );
// If the room ID of a previously connected call is still in settings at
// this time, that's a sign that we failed to disconnect from it
// properly, and need to clean up after ourselves
2024-12-23 20:25:15 +00:00
const uncleanlyDisconnectedRoomIds = SettingsStore . getValue ( "activeCallRoomIds" );
2022-08-30 15:13:39 -04:00
if ( uncleanlyDisconnectedRoomIds . length ) {
await Promise . all ([
2023-01-12 13:25:14 +00:00
... uncleanlyDisconnectedRoomIds . map ( async ( uncleanlyDisconnectedRoomId ) : Promise < void > => {
2022-08-30 15:13:39 -04:00
logger . log ( `Cleaning up call state for room ${ uncleanlyDisconnectedRoomId } ` );
2022-10-06 22:27:28 -04:00
await this . getCall ( uncleanlyDisconnectedRoomId ) ? . clean ();
2022-08-30 15:13:39 -04:00
}),
SettingsStore . setValue ( "activeCallRoomIds" , null , SettingLevel . DEVICE , []),
]);
}
}
protected async onNotReady () : Promise < any > {
for ( const [ call , listenerMap ] of this . callListeners ) {
// It's important that we remove the listeners before destroying the
// call, because otherwise the call's onDestroy callback would fire
// and immediately repopulate the map
for ( const [ event , listener ] of listenerMap ) call . off ( event , listener );
call . destroy ();
}
this . callListeners . clear ();
this . calls . clear ();
2024-06-17 13:00:41 +02:00
this . _connectedCalls . clear ();
2026-01-09 12:04:31 +00:00
this . configuredMatrixRTCTransports . clear ();
2022-08-30 15:13:39 -04:00
2025-09-23 11:45:48 -04:00
this . matrixClient ? . matrixRTC . off ( MatrixRTCSessionManagerEvents . SessionStarted , this . onRTCSessionStart );
2022-08-30 15:13:39 -04:00
WidgetStore . instance . off ( UPDATE_EVENT , this . onWidgets );
}
2024-06-17 13:00:41 +02:00
private _connectedCalls : Set < Call > = new Set ();
2022-08-30 15:13:39 -04:00
/**
* The calls to which the user is currently connected.
*/
2024-06-17 13:00:41 +02:00
public get connectedCalls () : Set < Call > {
return this . _connectedCalls ;
2022-08-30 15:13:39 -04:00
}
2024-06-17 13:00:41 +02:00
private set connectedCalls ( value : Set < Call >) {
this . _connectedCalls = value ;
this . emit ( CallStoreEvent . ConnectedCalls , value );
2022-08-30 15:13:39 -04:00
// The room IDs are persisted to settings so we can detect unclean disconnects
SettingsStore . setValue (
"activeCallRoomIds" ,
null ,
SettingLevel . DEVICE ,
[... value ]. map (( call ) => call . roomId ),
);
}
private calls = new Map < string , Call >(); // Key is room ID
private callListeners = new Map < Call , Map < CallEvent , ( ...args : unknown []) = > unknown > > ();
2025-09-23 11:45:48 -04:00
private inUpdateRoom = false ;
2023-01-12 13:25:14 +00:00
private updateRoom ( room : Room ) : void {
2025-09-23 11:45:48 -04:00
// XXX: This method is guarded with the flag this.inUpdateRoom because
// we need to block this method from calling itself recursively. That
// could happen, for instance, if Call.get adds a new virtual widget to
// the WidgetStore, firing a WidgetStore update that we don't actually
// care about. Without the guard we could get duplicate Call objects
// fighting for control over the same widget.
if ( ! this . inUpdateRoom && ! this . calls . has ( room . roomId )) {
this . inUpdateRoom = true ;
2022-08-30 15:13:39 -04:00
const call = Call . get ( room );
if ( call ) {
2023-01-12 13:25:14 +00:00
const onConnectionState = ( state : ConnectionState ) : void => {
2022-08-30 15:13:39 -04:00
if ( state === ConnectionState . Connected ) {
2024-06-17 13:00:41 +02:00
this . connectedCalls = new Set ([... this . connectedCalls , call ]);
2022-08-30 15:13:39 -04:00
} else if ( state === ConnectionState . Disconnected ) {
2024-06-17 13:00:41 +02:00
this . connectedCalls = new Set ([... this . connectedCalls ]. filter (( c ) => c !== call ));
2022-08-30 15:13:39 -04:00
}
};
2023-01-12 13:25:14 +00:00
const onDestroy = () : void => {
2022-08-30 15:13:39 -04:00
this . calls . delete ( room . roomId );
for ( const [ event , listener ] of this . callListeners . get ( call ) ! ) call . off ( event , listener );
this . updateRoom ( room );
};
call . on ( CallEvent . ConnectionState , onConnectionState );
call . on ( CallEvent . Destroy , onDestroy );
this . calls . set ( room . roomId , call );
this . callListeners . set (
call ,
2023-05-25 09:39:23 +01:00
new Map < CallEvent , ( ...args : any []) = > unknown > ([
2022-08-30 15:13:39 -04:00
[ CallEvent . ConnectionState , onConnectionState ],
[ CallEvent . Destroy , onDestroy ],
]),
);
}
this . emit ( CallStoreEvent . Call , call , room . roomId );
2025-09-23 11:45:48 -04:00
this . inUpdateRoom = false ;
2022-08-30 15:13:39 -04:00
}
}
/**
* Gets the call associated with the given room, if any.
* @param {string} roomId The room's ID.
* @returns {Call | null} The call.
*/
2022-10-06 22:27:28 -04:00
public getCall ( roomId : string ) : Call | null {
2022-08-30 15:13:39 -04:00
return this . calls . get ( roomId ) ?? null ;
}
2022-09-27 07:54:51 -04:00
/**
2022-10-06 22:27:28 -04:00
* Gets the active call associated with the given room, if any.
2022-09-27 07:54:51 -04:00
* @param roomId The room's ID.
2022-10-06 22:27:28 -04:00
* @returns The active call.
2022-09-27 07:54:51 -04:00
*/
2022-10-06 22:27:28 -04:00
public getActiveCall ( roomId : string ) : Call | null {
const call = this . getCall ( roomId );
2024-06-17 13:00:41 +02:00
return call !== null && this . connectedCalls . has ( call ) ? call : null ;
2022-09-27 07:54:51 -04:00
}
2023-01-12 13:25:14 +00:00
private onWidgets = ( roomId : string | null ) : void => {
2023-04-06 11:10:14 +01:00
if ( ! this . matrixClient ) return ;
2022-08-30 15:13:39 -04:00
if ( roomId === null ) {
// This store happened to start before the widget store was done
// loading all rooms, so we need to initialize each room again
for ( const room of this . matrixClient . getRooms ()) {
this . updateRoom ( room );
}
} else {
const room = this . matrixClient . getRoom ( roomId );
// Widget updates can arrive before the room does, empirically
if ( room !== null ) this . updateRoom ( room );
}
};
2022-11-28 16:37:32 -05:00
2026-01-09 12:04:31 +00:00
public getConfiguredRTCTransports () : Transport [] {
return [... this . configuredMatrixRTCTransports ];
}
2024-06-17 13:00:41 +02:00
private onRTCSessionStart = ( roomId : string , session : MatrixRTCSession ) : void => {
2023-10-30 16:14:27 +01:00
this . updateRoom ( session . room );
};
2022-08-30 15:13:39 -04:00
}