diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 7eb329594a..e032e4582d 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -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%; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index ce779f12a5..be687a4474 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -154,6 +154,9 @@ function getRemoteAudioElement(): HTMLAudioElement { export default class CallHandler { private calls = new Map(); // 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(); // callId (target) -> call (transferee) private audioPromises = new Map>(); 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[call.callId] = transferee; + } 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", diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 6b17d3ce60..a274f96a17 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -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"; @@ -332,6 +334,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, @@ -380,6 +383,7 @@ export default class InviteDialog extends React.PureComponent { + this.setState({consultFirst: ev.target.checked}); + } + static buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number}[] { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room @@ -745,16 +753,34 @@ export default class InviteDialog extends React.PureComponent; const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); @@ -1339,6 +1366,12 @@ export default class InviteDialog extends React.PureComponent + + ; } else { console.error("Unknown kind of InviteDialog: " + this.props.kind); } @@ -1375,6 +1408,7 @@ export default class InviteDialog extends React.PureComponent + {consultSection} ); diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 9bdc8fb11d..0a5d028069 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -364,6 +364,11 @@ export default class CallView extends React.Component { 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 { // 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 Switch") : _td("You held the call Resume"); - onHoldText = _t(holdString, {}, { - a: sub => - {sub} - , - }); - } 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 =
+ {_t( + "Consulting with %(transferTarget)s. Transfer to %(transferee)s", + { + transferTarget: transferTargetName, + transferee: transfereeName, + }, + { + a: sub => {sub}, + }, + )} +
; + } else if (isOnHold) { + let onHoldText = null; + if (this.state.isRemoteOnHold) { + const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ? + _td("You held the call Switch") : _td("You held the call Resume"); + onHoldText = _t(holdString, {}, { + a: sub => + {sub} + , + }); + } else if (this.state.isLocalOnHold) { + onHoldText = _t("%(peerName)s held the call", { + peerName: this.props.call.getOpponentMember().name, + }); + } + holdTransferContent =
+ {onHoldText} +
; } 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 { mx_CallView_video_hold: isOnHold, }); if (isOnHold) { - onHoldContent =
- {onHoldText} -
; 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 { maxHeight={maxVideoHeight} /> {localVideoFeed} - {onHoldContent} + {holdTransferContent} {callControls} ; } else { @@ -554,7 +583,7 @@ export default class CallView extends React.Component { /> -
{onHoldText}
+ {holdTransferContent} {callControls} ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a68b64ca4f..0397a3457c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -881,6 +881,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. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", "You held the call Switch": "You held the call Switch", "You held the call Resume": "You held the call Resume", "%(peerName)s held the call": "%(peerName)s held the call", @@ -2215,6 +2217,7 @@ "Invite someone using their name, username (like ) or share this room.": "Invite someone using their name, username (like ) or share this room.", "Invited people will be able to read old messages.": "Invited people will be able to read old messages.", "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",