WIP attended transfer
This commit is contained in:
parent
c0c8a55449
commit
82ba546142
5 changed files with 108 additions and 34 deletions
|
@ -55,7 +55,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_holdText {
|
||||
.mx_CallView_holdTransferContent {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_hold {
|
||||
.mx_CallView_voice .mx_CallView_holdTransferContent {
|
||||
// This masks the avatar image so when it's blurred, the edge is still crisp
|
||||
.mx_CallView_voice_avatarContainer {
|
||||
border-radius: 2000px;
|
||||
|
@ -91,7 +91,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_holdText {
|
||||
.mx_CallView_holdTransferContent {
|
||||
height: 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 15px;
|
||||
|
@ -142,7 +142,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_video_holdContent {
|
||||
.mx_CallView_video .mx_CallView_holdTransferContent {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
|
|
@ -154,6 +154,9 @@ function getRemoteAudioElement(): HTMLAudioElement {
|
|||
|
||||
export default class CallHandler {
|
||||
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||
// call with a different party to this one.
|
||||
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
|
||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||
private dispatcherRef: string = null;
|
||||
private supportsPstnProtocol = null;
|
||||
|
@ -325,6 +328,10 @@ export default class CallHandler {
|
|||
return callsNotInThatRoom;
|
||||
}
|
||||
|
||||
getTransfereeForCallId(callId: string): MatrixCall {
|
||||
return this.transferees[callId];
|
||||
}
|
||||
|
||||
play(audioId: AudioID) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
|
@ -622,6 +629,7 @@ export default class CallHandler {
|
|||
private async placeCall(
|
||||
roomId: string, type: PlaceCallType,
|
||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||
transferee: MatrixCall,
|
||||
) {
|
||||
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
||||
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
||||
|
@ -634,6 +642,9 @@ export default class CallHandler {
|
|||
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
|
||||
|
||||
this.calls.set(roomId, call);
|
||||
if (transferee) {
|
||||
this.transferees[transferee.callId] = call;
|
||||
}
|
||||
|
||||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
|
@ -723,7 +734,10 @@ export default class CallHandler {
|
|||
} else if (members.length === 2) {
|
||||
console.info(`Place ${payload.type} call in ${payload.room_id}`);
|
||||
|
||||
this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
|
||||
this.placeCall(
|
||||
payload.room_id, payload.type, payload.local_element, payload.remote_element,
|
||||
payload.transferee,
|
||||
);
|
||||
} else { // > 2
|
||||
dis.dispatch({
|
||||
action: "place_conference_call",
|
||||
|
|
|
@ -29,7 +29,9 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||
} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
@ -331,6 +333,7 @@ interface IInviteDialogState {
|
|||
threepidResultsMixin: { user: Member, userId: string}[];
|
||||
canUseIdentityServer: boolean;
|
||||
tryingIdentityServer: boolean;
|
||||
consultFirst: boolean;
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: boolean,
|
||||
|
@ -379,6 +382,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
threepidResultsMixin: [],
|
||||
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
tryingIdentityServer: false,
|
||||
consultFirst: false,
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: false,
|
||||
|
@ -394,6 +398,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
}
|
||||
|
||||
private onConsultFirstChange = (ev) => {
|
||||
this.setState({consultFirst: ev.target.checked});
|
||||
}
|
||||
|
||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
|
@ -721,16 +729,28 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
});
|
||||
}
|
||||
|
||||
this.setState({busy: true});
|
||||
try {
|
||||
await this.props.call.transfer(targetIds[0]);
|
||||
this.setState({busy: false});
|
||||
this.props.onFinished();
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to transfer call"),
|
||||
if (this.state.consultFirst) {
|
||||
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: this.props.call.type,
|
||||
room_id: dmRoomId,
|
||||
transferee: this.props.call,
|
||||
});
|
||||
this.props.onFinished();
|
||||
} else {
|
||||
this.setState({busy: true});
|
||||
try {
|
||||
await this.props.call.transfer(targetIds[0]);
|
||||
this.setState({busy: false});
|
||||
this.props.onFinished();
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to transfer call"),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1189,6 +1209,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn;
|
||||
let consultSection;
|
||||
|
||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||
|
||||
|
@ -1292,6 +1313,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
goButtonFn = this._transferCall;
|
||||
consultSection = <div>
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||
{_t("Consult first")}
|
||||
</label>
|
||||
</div>;
|
||||
} else {
|
||||
console.error("Unknown kind of InviteDialog: " + this.props.kind);
|
||||
}
|
||||
|
@ -1327,6 +1354,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
{this._renderSection('recents')}
|
||||
{this._renderSection('suggestions')}
|
||||
</div>
|
||||
{consultSection}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -364,6 +364,11 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||
}
|
||||
|
||||
private onTransferClick = () => {
|
||||
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
|
||||
this.props.call.transferToCall(transfereeCall);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
|
@ -479,25 +484,52 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
// for voice calls (fills the bg)
|
||||
let contentView: React.ReactNode;
|
||||
|
||||
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
|
||||
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
|
||||
let onHoldText = null;
|
||||
if (this.state.isRemoteOnHold) {
|
||||
const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
|
||||
_td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
|
||||
onHoldText = _t(holdString, {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
|
||||
{sub}
|
||||
</AccessibleButton>,
|
||||
});
|
||||
} else if (this.state.isLocalOnHold) {
|
||||
onHoldText = _t("%(peerName)s held the call", {
|
||||
peerName: this.props.call.getOpponentMember().name,
|
||||
});
|
||||
let holdTransferContent;
|
||||
if (transfereeCall) {
|
||||
const transferTargetRoom = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.props.call));
|
||||
const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
|
||||
|
||||
const transfereeRoom = MatrixClientPeg.get().getRoom(
|
||||
CallHandler.roomIdForCall(transfereeCall),
|
||||
);
|
||||
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
|
||||
|
||||
holdTransferContent = <div className="mx_CallView_holdTransferContent">
|
||||
{_t(
|
||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
||||
{
|
||||
transferTarget: transferTargetName,
|
||||
transferee: transfereeName,
|
||||
},
|
||||
{
|
||||
a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>{sub}</AccessibleButton>,
|
||||
},
|
||||
)}
|
||||
</div>;
|
||||
} else if (isOnHold) {
|
||||
let onHoldText = null;
|
||||
if (this.state.isRemoteOnHold) {
|
||||
const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
|
||||
_td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
|
||||
onHoldText = _t(holdString, {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
|
||||
{sub}
|
||||
</AccessibleButton>,
|
||||
});
|
||||
} else if (this.state.isLocalOnHold) {
|
||||
onHoldText = _t("%(peerName)s held the call", {
|
||||
peerName: this.props.call.getOpponentMember().name,
|
||||
});
|
||||
}
|
||||
holdTransferContent = <div className="mx_CallView_holdTransferContent">
|
||||
{onHoldText}
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
let localVideoFeed = null;
|
||||
let onHoldContent = null;
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
const containerClasses = classNames({
|
||||
|
@ -505,9 +537,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
mx_CallView_video_hold: isOnHold,
|
||||
});
|
||||
if (isOnHold) {
|
||||
onHoldContent = <div className="mx_CallView_video_holdContent">
|
||||
{onHoldText}
|
||||
</div>;
|
||||
const backgroundAvatarUrl = avatarUrlForMember(
|
||||
// is it worth getting the size of the div to pass here?
|
||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||
|
@ -534,7 +563,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
maxHeight={maxVideoHeight}
|
||||
/>
|
||||
{localVideoFeed}
|
||||
{onHoldContent}
|
||||
{holdTransferContent}
|
||||
{callControls}
|
||||
</div>;
|
||||
} else {
|
||||
|
@ -554,7 +583,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
|
||||
{holdTransferContent}
|
||||
{callControls}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -876,6 +876,8 @@
|
|||
"sends fireworks": "sends fireworks",
|
||||
"Sends the given message with snowfall": "Sends the given message with snowfall",
|
||||
"sends snowfall": "sends snowfall",
|
||||
"unknown person": "unknown person",
|
||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
||||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||
|
@ -2208,6 +2210,7 @@
|
|||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
|
||||
"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>.",
|
||||
"Transfer": "Transfer",
|
||||
"Consult first": "Consult first",
|
||||
"a new master key signature": "a new master key signature",
|
||||
"a new cross-signing key signature": "a new cross-signing key signature",
|
||||
"a device cross-signing signature": "a device cross-signing signature",
|
||||
|
|
Loading…
Reference in a new issue