Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit cd39474

Browse files
authored
Merge pull request #5798 from matrix-org/dbkr/attended_transfer
Attended transfer
2 parents 151749b + 299467c commit cd39474

File tree

5 files changed

+114
-34
lines changed

5 files changed

+114
-34
lines changed

res/css/views/voip/_CallView.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ limitations under the License.
5555
}
5656
}
5757

58-
.mx_CallView_voice_holdText {
58+
.mx_CallView_holdTransferContent {
5959
padding-top: 10px;
6060
padding-bottom: 25px;
6161
}
@@ -82,7 +82,7 @@ limitations under the License.
8282
}
8383
}
8484

85-
.mx_CallView_voice_hold {
85+
.mx_CallView_voice .mx_CallView_holdTransferContent {
8686
// This masks the avatar image so when it's blurred, the edge is still crisp
8787
.mx_CallView_voice_avatarContainer {
8888
border-radius: 2000px;
@@ -91,7 +91,7 @@ limitations under the License.
9191
}
9292
}
9393

94-
.mx_CallView_voice_holdText {
94+
.mx_CallView_holdTransferContent {
9595
height: 20px;
9696
padding-top: 20px;
9797
padding-bottom: 15px;
@@ -142,7 +142,7 @@ limitations under the License.
142142
}
143143
}
144144

145-
.mx_CallView_video_holdContent {
145+
.mx_CallView_video .mx_CallView_holdTransferContent {
146146
position: absolute;
147147
top: 50%;
148148
left: 50%;

src/CallHandler.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ function getRemoteAudioElement(): HTMLAudioElement {
154154

155155
export default class CallHandler {
156156
private calls = new Map<string, MatrixCall>(); // roomId -> call
157+
// Calls started as an attended transfer, ie. with the intention of transferring another
158+
// call with a different party to this one.
159+
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
157160
private audioPromises = new Map<AudioID, Promise<void>>();
158161
private dispatcherRef: string = null;
159162
private supportsPstnProtocol = null;
@@ -325,6 +328,10 @@ export default class CallHandler {
325328
return callsNotInThatRoom;
326329
}
327330

331+
getTransfereeForCallId(callId: string): MatrixCall {
332+
return this.transferees[callId];
333+
}
334+
328335
play(audioId: AudioID) {
329336
// TODO: Attach an invisible element for this instead
330337
// which listens?
@@ -622,6 +629,7 @@ export default class CallHandler {
622629
private async placeCall(
623630
roomId: string, type: PlaceCallType,
624631
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
632+
transferee: MatrixCall,
625633
) {
626634
Analytics.trackEvent('voip', 'placeCall', 'type', type);
627635
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
@@ -634,6 +642,9 @@ export default class CallHandler {
634642
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
635643

636644
this.calls.set(roomId, call);
645+
if (transferee) {
646+
this.transferees[call.callId] = transferee;
647+
}
637648

638649
this.setCallListeners(call);
639650
this.setCallAudioElement(call);
@@ -723,7 +734,10 @@ export default class CallHandler {
723734
} else if (members.length === 2) {
724735
console.info(`Place ${payload.type} call in ${payload.room_id}`);
725736

726-
this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
737+
this.placeCall(
738+
payload.room_id, payload.type, payload.local_element, payload.remote_element,
739+
payload.transferee,
740+
);
727741
} else { // > 2
728742
dis.dispatch({
729743
action: "place_conference_call",

src/components/views/dialogs/InviteDialog.tsx

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import dis from "../../../dispatcher/dispatcher";
2929
import IdentityAuthClient from "../../../IdentityAuthClient";
3030
import Modal from "../../../Modal";
3131
import {humanizeTime} from "../../../utils/humanize";
32-
import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom";
32+
import createRoom, {
33+
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
34+
} from "../../../createRoom";
3335
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
3436
import {Key} from "../../../Keyboard";
3537
import {Action} from "../../../dispatcher/actions";
@@ -332,6 +334,7 @@ interface IInviteDialogState {
332334
threepidResultsMixin: { user: Member, userId: string}[];
333335
canUseIdentityServer: boolean;
334336
tryingIdentityServer: boolean;
337+
consultFirst: boolean;
335338

336339
// These two flags are used for the 'Go' button to communicate what is going on.
337340
busy: boolean,
@@ -380,6 +383,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
380383
threepidResultsMixin: [],
381384
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
382385
tryingIdentityServer: false,
386+
consultFirst: false,
383387

384388
// These two flags are used for the 'Go' button to communicate what is going on.
385389
busy: false,
@@ -395,6 +399,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
395399
}
396400
}
397401

402+
private onConsultFirstChange = (ev) => {
403+
this.setState({consultFirst: ev.target.checked});
404+
}
405+
398406
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
399407
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
400408

@@ -745,16 +753,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
745753
});
746754
}
747755

748-
this.setState({busy: true});
749-
try {
750-
await this.props.call.transfer(targetIds[0]);
751-
this.setState({busy: false});
752-
this.props.onFinished();
753-
} catch (e) {
754-
this.setState({
755-
busy: false,
756-
errorText: _t("Failed to transfer call"),
756+
if (this.state.consultFirst) {
757+
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
758+
759+
dis.dispatch({
760+
action: 'place_call',
761+
type: this.props.call.type,
762+
room_id: dmRoomId,
763+
transferee: this.props.call,
764+
});
765+
dis.dispatch({
766+
action: 'view_room',
767+
room_id: dmRoomId,
768+
should_peek: false,
769+
joining: false,
757770
});
771+
this.props.onFinished();
772+
} else {
773+
this.setState({busy: true});
774+
try {
775+
await this.props.call.transfer(targetIds[0]);
776+
this.setState({busy: false});
777+
this.props.onFinished();
778+
} catch (e) {
779+
this.setState({
780+
busy: false,
781+
errorText: _t("Failed to transfer call"),
782+
});
783+
}
758784
}
759785
};
760786

@@ -1215,6 +1241,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
12151241
let helpText;
12161242
let buttonText;
12171243
let goButtonFn;
1244+
let consultSection;
12181245
let keySharingWarning = <span />;
12191246

12201247
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
@@ -1339,6 +1366,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
13391366
title = _t("Transfer");
13401367
buttonText = _t("Transfer");
13411368
goButtonFn = this._transferCall;
1369+
consultSection = <div>
1370+
<label>
1371+
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
1372+
{_t("Consult first")}
1373+
</label>
1374+
</div>;
13421375
} else {
13431376
console.error("Unknown kind of InviteDialog: " + this.props.kind);
13441377
}
@@ -1375,6 +1408,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
13751408
{this._renderSection('recents')}
13761409
{this._renderSection('suggestions')}
13771410
</div>
1411+
{consultSection}
13781412
</div>
13791413
</BaseDialog>
13801414
);

src/components/views/voip/CallView.tsx

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ export default class CallView extends React.Component<IProps, IState> {
364364
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
365365
}
366366

367+
private onTransferClick = () => {
368+
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
369+
this.props.call.transferToCall(transfereeCall);
370+
}
371+
367372
public render() {
368373
const client = MatrixClientPeg.get();
369374
const callRoomId = CallHandler.roomIdForCall(this.props.call);
@@ -479,35 +484,59 @@ export default class CallView extends React.Component<IProps, IState> {
479484
// for voice calls (fills the bg)
480485
let contentView: React.ReactNode;
481486

487+
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
482488
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
483-
let onHoldText = null;
484-
if (this.state.isRemoteOnHold) {
485-
const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
486-
_td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
487-
onHoldText = _t(holdString, {}, {
488-
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
489-
{sub}
490-
</AccessibleButton>,
491-
});
492-
} else if (this.state.isLocalOnHold) {
493-
onHoldText = _t("%(peerName)s held the call", {
494-
peerName: this.props.call.getOpponentMember().name,
495-
});
489+
let holdTransferContent;
490+
if (transfereeCall) {
491+
const transferTargetRoom = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.props.call));
492+
const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
493+
494+
const transfereeRoom = MatrixClientPeg.get().getRoom(
495+
CallHandler.roomIdForCall(transfereeCall),
496+
);
497+
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
498+
499+
holdTransferContent = <div className="mx_CallView_holdTransferContent">
500+
{_t(
501+
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
502+
{
503+
transferTarget: transferTargetName,
504+
transferee: transfereeName,
505+
},
506+
{
507+
a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>{sub}</AccessibleButton>,
508+
},
509+
)}
510+
</div>;
511+
} else if (isOnHold) {
512+
let onHoldText = null;
513+
if (this.state.isRemoteOnHold) {
514+
const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
515+
_td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
516+
onHoldText = _t(holdString, {}, {
517+
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
518+
{sub}
519+
</AccessibleButton>,
520+
});
521+
} else if (this.state.isLocalOnHold) {
522+
onHoldText = _t("%(peerName)s held the call", {
523+
peerName: this.props.call.getOpponentMember().name,
524+
});
525+
}
526+
holdTransferContent = <div className="mx_CallView_holdTransferContent">
527+
{onHoldText}
528+
</div>;
496529
}
497530

498531
if (this.props.call.type === CallType.Video) {
499532
let localVideoFeed = null;
500-
let onHoldContent = null;
501533
let onHoldBackground = null;
502534
const backgroundStyle: CSSProperties = {};
503535
const containerClasses = classNames({
504536
mx_CallView_video: true,
505537
mx_CallView_video_hold: isOnHold,
506538
});
507539
if (isOnHold) {
508-
onHoldContent = <div className="mx_CallView_video_holdContent">
509-
{onHoldText}
510-
</div>;
511540
const backgroundAvatarUrl = avatarUrlForMember(
512541
// is it worth getting the size of the div to pass here?
513542
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
@@ -534,7 +563,7 @@ export default class CallView extends React.Component<IProps, IState> {
534563
maxHeight={maxVideoHeight}
535564
/>
536565
{localVideoFeed}
537-
{onHoldContent}
566+
{holdTransferContent}
538567
{callControls}
539568
</div>;
540569
} else {
@@ -554,7 +583,7 @@ export default class CallView extends React.Component<IProps, IState> {
554583
/>
555584
</div>
556585
</div>
557-
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
586+
{holdTransferContent}
558587
{callControls}
559588
</div>;
560589
}

src/i18n/strings/en_EN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,8 @@
881881
"sends fireworks": "sends fireworks",
882882
"Sends the given message with snowfall": "Sends the given message with snowfall",
883883
"sends snowfall": "sends snowfall",
884+
"unknown person": "unknown person",
885+
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
884886
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
885887
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
886888
"%(peerName)s held the call": "%(peerName)s held the call",
@@ -2215,6 +2217,7 @@
22152217
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
22162218
"Invited people will be able to read old messages.": "Invited people will be able to read old messages.",
22172219
"Transfer": "Transfer",
2220+
"Consult first": "Consult first",
22182221
"a new master key signature": "a new master key signature",
22192222
"a new cross-signing key signature": "a new cross-signing key signature",
22202223
"a device cross-signing signature": "a device cross-signing signature",

0 commit comments

Comments
 (0)