Add VoIP user mapper
The accompanying element-web PR with the config documentation should explain what this is & why. Internally, this breaks the assumption that call.roomId is the room that the call appears in for the user. call.roomId may now be a 'virtual' room while the react SDK actually displays it in a different room. React SDK always stores the calls under the user-facing rooms, and provides a function to get the user-facing room for a given call.
This commit is contained in:
parent
604b9378ce
commit
0a90c982c7
8 changed files with 173 additions and 32 deletions
|
@ -83,6 +83,7 @@ import {UIFeature} from "./settings/UIFeature";
|
|||
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { Action } from './dispatcher/actions';
|
||||
import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper';
|
||||
|
||||
const CHECK_PSTN_SUPPORT_ATTEMPTS = 3;
|
||||
|
||||
|
@ -133,6 +134,15 @@ export default class CallHandler {
|
|||
return window.mxCallHandler;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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)
|
||||
*/
|
||||
public static roomIdForCall(call: MatrixCall) {
|
||||
if (!call) return null;
|
||||
return roomForVirtualRoom(call.roomId) || call.roomId;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
// add empty handlers for media actions, otherwise the media keys
|
||||
|
@ -284,11 +294,15 @@ export default class CallHandler {
|
|||
// 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.
|
||||
const callForThisRoom = this.getCallForRoom(call.roomId);
|
||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
||||
|
||||
const callForThisRoom = this.getCallForRoom(mappedRoomId);
|
||||
return callForThisRoom && call.callId === callForThisRoom.callId;
|
||||
}
|
||||
|
||||
private setCallListeners(call: MatrixCall) {
|
||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
||||
|
||||
call.on(CallEvent.Error, (err: CallError) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
|
@ -318,7 +332,7 @@ export default class CallHandler {
|
|||
|
||||
Analytics.trackEvent('voip', 'callHangup');
|
||||
|
||||
this.removeCallForRoom(call.roomId);
|
||||
this.removeCallForRoom(mappedRoomId);
|
||||
});
|
||||
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
@ -343,7 +357,7 @@ export default class CallHandler {
|
|||
break;
|
||||
case CallState.Ended:
|
||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
|
||||
this.removeCallForRoom(call.roomId);
|
||||
this.removeCallForRoom(mappedRoomId);
|
||||
if (oldState === CallState.InviteSent && (
|
||||
call.hangupParty === CallParty.Remote ||
|
||||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
|
||||
|
@ -392,7 +406,7 @@ export default class CallHandler {
|
|||
this.pause(AudioID.Ringback);
|
||||
}
|
||||
|
||||
this.calls.set(newCall.roomId, newCall);
|
||||
this.calls.set(mappedRoomId, newCall);
|
||||
this.setCallListeners(newCall);
|
||||
this.setCallState(newCall, newCall.state);
|
||||
});
|
||||
|
@ -404,13 +418,15 @@ export default class CallHandler {
|
|||
}
|
||||
|
||||
private setCallState(call: MatrixCall, status: CallState) {
|
||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
||||
|
||||
console.log(
|
||||
`Call state in ${call.roomId} changed to ${status}`,
|
||||
`Call state in ${mappedRoomId} changed to ${status}`,
|
||||
);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'call_state',
|
||||
room_id: call.roomId,
|
||||
room_id: mappedRoomId,
|
||||
state: status,
|
||||
});
|
||||
}
|
||||
|
@ -477,14 +493,20 @@ export default class CallHandler {
|
|||
}, null, true);
|
||||
}
|
||||
|
||||
private placeCall(
|
||||
private async placeCall(
|
||||
roomId: string, type: PlaceCallType,
|
||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||
) {
|
||||
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
||||
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
||||
const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
|
||||
|
||||
const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || roomId;
|
||||
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
|
||||
|
||||
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
|
||||
|
||||
this.calls.set(roomId, call);
|
||||
|
||||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
|
||||
|
@ -586,13 +608,14 @@ export default class CallHandler {
|
|||
|
||||
const call = payload.call as MatrixCall;
|
||||
|
||||
if (this.getCallForRoom(call.roomId)) {
|
||||
const mappedRoomId = CallHandler.roomIdForCall(call);
|
||||
if (this.getCallForRoom(mappedRoomId)) {
|
||||
// ignore multiple incoming calls to the same room
|
||||
return;
|
||||
}
|
||||
|
||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||
this.calls.set(call.roomId, call)
|
||||
this.calls.set(mappedRoomId, call)
|
||||
this.setCallListeners(call);
|
||||
}
|
||||
break;
|
||||
|
|
77
src/VoipUserMapper.ts
Normal file
77
src/VoipUserMapper.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
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 { ensureDMExists, findDMForUser } from './createRoom';
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
|
||||
// Functions for mapping users & rooms for the voip_mxid_translate_pattern
|
||||
// config option
|
||||
|
||||
export function voipUserMapperEnabled(): boolean {
|
||||
return SdkConfig.get()['voip_mxid_translate_pattern'] !== undefined;
|
||||
}
|
||||
|
||||
function userToVirtualUser(userId: string, templateString?: string): string {
|
||||
if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
|
||||
if (!templateString) return null;
|
||||
return templateString.replace('${mxid}', encodeURIComponent(userId).replace(/%/g, '=').toLowerCase());
|
||||
}
|
||||
|
||||
function virtualUserToUser(userId: string, templateString?: string): string {
|
||||
if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
|
||||
if (!templateString) return null;
|
||||
|
||||
const regexString = templateString.replace('${mxid}', '(.+)');
|
||||
|
||||
const match = userId.match('^' + regexString + '$');
|
||||
if (!match) return null;
|
||||
|
||||
return decodeURIComponent(match[1].replace(/=/g, '%'));
|
||||
}
|
||||
|
||||
async function getOrCreateVirtualRoomForUser(userId: string):Promise<string> {
|
||||
const virtualUser = userToVirtualUser(userId);
|
||||
if (!virtualUser) return null;
|
||||
|
||||
return await ensureDMExists(MatrixClientPeg.get(), virtualUser);
|
||||
}
|
||||
|
||||
export async function getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> {
|
||||
const user = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (!user) return null;
|
||||
return getOrCreateVirtualRoomForUser(user);
|
||||
}
|
||||
|
||||
export function roomForVirtualRoom(roomId: string):string {
|
||||
const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (!virtualUser) return null;
|
||||
const realUser = virtualUserToUser(virtualUser);
|
||||
const room = findDMForUser(MatrixClientPeg.get(), realUser);
|
||||
if (room) {
|
||||
return room.roomId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isVirtualRoom(roomId: string):boolean {
|
||||
const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (!virtualUser) return null;
|
||||
const realUser = virtualUserToUser(virtualUser);
|
||||
return Boolean(realUser);
|
||||
}
|
|
@ -109,9 +109,12 @@ function HangupButton(props) {
|
|||
|
||||
dis.dispatch({
|
||||
action,
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId,
|
||||
// hangup the call for this room. NB. We use the room in props as the room ID
|
||||
// as call.roomId may be the 'virtual room', and the dispatch actions always
|
||||
// use the user-facing room (there was a time when we deliberately used
|
||||
// call.roomId and *not* props.roomId, but that was for the old
|
||||
// style Freeswitch conference calls and those times are gone.)
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -212,9 +212,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onExpandClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -340,27 +341,33 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onRoomAvatarClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onSecondaryRoomAvatarClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.secondaryCall.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onCallResumeClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoom = client.getRoom(this.props.call.roomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null;
|
||||
const callRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
const secondaryCallRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
|
||||
const callRoom = client.getRoom(callRoomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
|
||||
|
||||
let dialPad;
|
||||
let contextMenu;
|
||||
|
@ -456,7 +463,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: callRoomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'answer',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -78,7 +78,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'reject',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -89,7 +89,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
|
||||
let room = null;
|
||||
if (this.state.incomingCall) {
|
||||
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
|
||||
room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall));
|
||||
}
|
||||
|
||||
const caller = room ? room.name : _t("Unknown caller");
|
||||
|
|
|
@ -398,6 +398,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: null,
|
||||
},
|
||||
"voip_mxid_translate_pattern": {
|
||||
supportedLevels: LEVELS_UI_FEATURE, // not a UI feature, but same level (config only, maybe .well-known in future)
|
||||
default: null,
|
||||
},
|
||||
"language": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
default: "en",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
||||
import { isVirtualRoom, voipUserMapperEnabled } from "../../../VoipUserMapper";
|
||||
|
||||
export class VisibilityProvider {
|
||||
private static internalInstance: VisibilityProvider;
|
||||
|
@ -31,18 +32,13 @@ export class VisibilityProvider {
|
|||
}
|
||||
|
||||
public isRoomVisible(room: Room): boolean {
|
||||
/* eslint-disable prefer-const */
|
||||
let isVisible = true; // Returned at the end of this function
|
||||
let forced = false; // When true, this function won't bother calling the customisation points
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
// ------
|
||||
// TODO: The `if` statements to control visibility of custom room types
|
||||
// would go here. The remainder of this function assumes that the statements
|
||||
// will be here.
|
||||
//
|
||||
// When removing this comment block, please remove the lint disable lines in the area.
|
||||
// ------
|
||||
if (voipUserMapperEnabled() && isVirtualRoom(room.roomId)) {
|
||||
isVisible = false;
|
||||
forced = true;
|
||||
}
|
||||
|
||||
const isVisibleFn = RoomListCustomisations.isRoomVisible;
|
||||
if (!forced && isVisibleFn) {
|
||||
|
|
31
test/VoipUserMapper-test.ts
Normal file
31
test/VoipUserMapper-test.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { userToVirtualUser, virtualUserToUser } from '../src/VoipUserMapper';
|
||||
|
||||
const templateString = '@_greatappservice_${mxid}:frooble.example';
|
||||
const realUser = '@alice:boop.example';
|
||||
const virtualUser = "@_greatappservice_%40alice%3Aboop.example:frooble.example";
|
||||
|
||||
describe('VoipUserMapper', function() {
|
||||
it('translates users to virtual users', function() {
|
||||
expect(userToVirtualUser(realUser, templateString)).toEqual(virtualUser);
|
||||
});
|
||||
|
||||
it('translates users to virtual users', function() {
|
||||
expect(virtualUserToUser(virtualUser, templateString)).toEqual(realUser);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue