diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 54c7df3e0bc..f2ceb086c4f 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -43,6 +43,14 @@ limitations under the License. } } + &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-video.svg'); + } + .mx_CallEvent_info { display: flex; flex-direction: row; diff --git a/res/img/voip/missed-video.svg b/res/img/voip/missed-video.svg new file mode 100644 index 00000000000..a2f3bc73ac5 --- /dev/null +++ b/res/img/voip/missed-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/missed-voice.svg b/res/img/voip/missed-voice.svg new file mode 100644 index 00000000000..5e3993584e0 --- /dev/null +++ b/res/img/voip/missed-voice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e7ba1aa9fb2..72be3f8e67c 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -491,28 +491,18 @@ export default class CallHandler extends EventEmitter { break; case CallState.Ended: { - Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); + const hangupReason = call.hangupReason; + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason); this.removeCallForRoom(mappedRoomId); - if (oldState === CallState.InviteSent && ( - call.hangupParty === CallParty.Remote || - (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) - )) { + if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) { this.play(AudioID.Busy); let title; let description; - if (call.hangupReason === CallErrorCode.UserHangup) { - title = _t("Call Declined"); - description = _t("The other party declined the call."); - } else if (call.hangupReason === CallErrorCode.UserBusy) { + // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) + if (call.hangupReason === CallErrorCode.UserBusy) { title = _t("User Busy"); description = _t("The user you called is busy."); - } else if (call.hangupReason === CallErrorCode.InviteTimeout) { - title = _t("Call Failed"); - // XXX: full stop appended as some relic here, but these - // strings need proper input from design anyway, so let's - // not change this string until we have a proper one. - description = _t('The remote side failed to pick up') + '.'; - } else { + } else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) { title = _t("Call Failed"); description = _t("The call could not be established"); } @@ -521,7 +511,7 @@ export default class CallHandler extends EventEmitter { title, description, }); } else if ( - call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting + hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting ) { Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { title: _t("Answered Elsewhere"), diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 384f20cd4e9..ce3b5308585 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -74,6 +74,14 @@ export default class CallEventGrouper extends EventEmitter { return this.hangup?.getContent()?.reason; } + public get rejectParty(): string { + return this.reject?.getSender(); + } + + public get gotRejected(): boolean { + return Boolean(this.reject); + } + /** * Returns true if there are only events from the other side - we missed the call */ diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index c81055bfb7a..2de66f897a7 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; import classNames from 'classnames'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; interface IProps { mxEvent: MatrixEvent; @@ -69,6 +70,18 @@ export default class CallEvent extends React.Component { this.setState({ callState: newState }); }; + private renderCallBackButton(text: string): JSX.Element { + return ( + + { text } + + ); + } + private renderContent(state: CallState | CustomCallState): JSX.Element { if (state === CallState.Ringing) { const silenceClass = classNames({ @@ -103,8 +116,18 @@ export default class CallEvent extends React.Component { } if (state === CallState.Ended) { const hangupReason = this.props.callEventGrouper.hangupReason; + const gotRejected = this.props.callEventGrouper.gotRejected; + const rejectParty = this.props.callEventGrouper.rejectParty; - if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) { + if (gotRejected) { + const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty; + return ( +
+ { weDeclinedCall ? _t("You declined this call") : _t("They declined this call") } + { this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) } +
+ ); + } else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) { // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( @@ -116,6 +139,13 @@ export default class CallEvent extends React.Component { { _t("This call has ended") } ); + } else if (hangupReason === CallErrorCode.InviteTimeout) { + return ( +
+ { _t("They didn't pick up") } + { this.renderCallBackButton(_t("Call again")) } +
+ ); } let reason; @@ -133,8 +163,6 @@ export default class CallEvent extends React.Component { // (as opposed to an error code they gave but we don't know about, // in which case we show the error code) reason = _t("An unknown error occurred"); - } else if (hangupReason === CallErrorCode.InviteTimeout) { - reason = _t("No answer"); } else if (hangupReason === CallErrorCode.UserBusy) { reason = _t("The user you called is busy."); } else { @@ -163,13 +191,7 @@ export default class CallEvent extends React.Component { return (
{ _t("You missed this call") } - - { _t("Call back") } - + { this.renderCallBackButton(_t("Call back")) }
); } @@ -186,11 +208,17 @@ export default class CallEvent extends React.Component { const sender = event.sender ? event.sender.name : event.getSender(); const isVoice = this.props.callEventGrouper.isVoice; const callType = isVoice ? _t("Voice call") : _t("Video call"); - const content = this.renderContent(this.state.callState); + const callState = this.state.callState; + const hangupReason = this.props.callEventGrouper.hangupReason; + const content = this.renderContent(callState); const className = classNames({ mx_CallEvent: true, mx_CallEvent_voice: isVoice, mx_CallEvent_video: !isVoice, + mx_CallEvent_missed: ( + callState === CustomCallState.Missed || + (callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout) + ), }); return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b36910b41bc..102a481f520 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -35,11 +35,8 @@ "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", "Call Failed": "Call Failed", - "Call Declined": "Call Declined", - "The other party declined the call.": "The other party declined the call.", "User Busy": "User Busy", "The user you called is busy.": "The user you called is busy.", - "The remote side failed to pick up": "The remote side failed to pick up", "The call could not be established": "The call could not be established", "Answered Elsewhere": "Answered Elsewhere", "The call was answered on another device.": "The call was answered on another device.", @@ -1862,16 +1859,19 @@ "Verification cancelled": "Verification cancelled", "Compare emoji": "Compare emoji", "Connected": "Connected", + "You declined this call": "You declined this call", + "They declined this call": "They declined this call", + "Call back": "Call back", + "Call again": "Call again", "This call has ended": "This call has ended", + "They didn't pick up": "They didn't pick up", "Could not connect media": "Could not connect media", "Connection failed": "Connection failed", "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", "An unknown error occurred": "An unknown error occurred", - "No answer": "No answer", "Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)", "This call has failed": "This call has failed", "You missed this call": "You missed this call", - "Call back": "Call back", "The call is in an unknown state!": "The call is in an unknown state!", "Sunday": "Sunday", "Monday": "Monday",