diff --git a/res/css/_components.scss b/res/css/_components.scss index 76551b51f8c..92d2bfe7f37 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -266,6 +266,7 @@ @import "./views/spaces/_SpacePublicShare.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/toasts/_AnalyticsToast.scss"; +@import "./views/toasts/_IncomingCallToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallContainer.scss"; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index d2485687402..2c3f1c705c4 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -28,7 +28,7 @@ limitations under the License. margin: 0 4px; grid-row: 2 / 4; grid-column: 1; - background-color: $dark-panel-bg-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; } @@ -37,7 +37,7 @@ limitations under the License. grid-row: 1 / 3; grid-column: 1; color: $primary-fg-color; - background-color: $dark-panel-bg-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss new file mode 100644 index 00000000000..975628f9486 --- /dev/null +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -0,0 +1,149 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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. +*/ + +.mx_IncomingCallToast { + display: flex; + flex-direction: row; + pointer-events: initial; // restore pointer events so the user can accept/decline + + .mx_IncomingCallToast_content { + display: flex; + flex-direction: column; + margin-left: 8px; + + .mx_CallEvent_caller { + font-weight: bold; + font-size: $font-15px; + line-height: $font-18px; + + margin-top: 2px; + } + + .mx_CallEvent_type { + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + + margin-top: 4px; + margin-bottom: 6px; + + display: flex; + flex-direction: row; + align-items: center; + + .mx_CallEvent_type_icon { + height: 16px; + width: 16px; + margin-right: 6px; + + &::before { + content: ''; + position: absolute; + height: inherit; + width: inherit; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + + &.mx_IncomingCallToast_content_voice { + .mx_CallEvent_type .mx_CallEvent_type_icon::before, + .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_IncomingCallToast_content_video { + .mx_CallEvent_type .mx_CallEvent_type_icon::before, + .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + + .mx_IncomingCallToast_buttons { + margin-top: 8px; + display: flex; + flex-direction: row; + gap: 12px; + + .mx_IncomingCallToast_button { + height: 24px; + padding: 0px 8px; + flex-shrink: 0; + flex-grow: 1; + margin-right: 0; + font-size: $font-15px; + line-height: $font-24px; + + span { + padding: 8px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + margin-right: 8px; + } + } + + &.mx_IncomingCallToast_button_accept span::before { + mask-size: 13px; + width: 13px; + height: 13px; + } + + &.mx_IncomingCallToast_button_decline span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + mask-size: 16px; + width: 16px; + height: 16px; + } + } + } + } + + .mx_IncomingCallToast_iconButton { + display: flex; + height: 20px; + width: 20px; + + &::before { + content: ''; + + height: inherit; + width: inherit; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_IncomingCallToast_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallToast_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } +} diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 0c09070334a..181a5ee0a32 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -43,84 +43,4 @@ limitations under the License. .mx_AppTile_persistedWrapper div { min-width: 350px; } - - .mx_IncomingCallBox { - min-width: 250px; - background-color: $voipcall-plinth-color; - padding: 8px; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - border-radius: 8px; - - pointer-events: initial; // restore pointer events so the user can accept/decline - cursor: pointer; - - .mx_IncomingCallBox_CallerInfo { - display: flex; - direction: row; - - img, .mx_BaseAvatar_initial { - margin: 8px; - } - - > div { - display: flex; - flex-direction: column; - - justify-content: center; - } - - h1, p { - margin: 0px; - padding: 0px; - font-size: $font-14px; - line-height: $font-16px; - } - - h1 { - font-weight: bold; - } - } - - .mx_IncomingCallBox_buttons { - padding: 8px; - display: flex; - flex-direction: row; - - > .mx_IncomingCallBox_spacer { - width: 8px; - } - - > * { - flex-shrink: 0; - flex-grow: 1; - margin-right: 0; - font-size: $font-15px; - line-height: $font-24px; - } - } - - .mx_IncomingCallBox_iconButton { - position: absolute; - right: 8px; - - &::before { - content: ''; - - height: 20px; - width: 20px; - background-color: $icon-button-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - } - - .mx_IncomingCallBox_silence::before { - mask-image: url('$(res)/img/voip/silence.svg'); - } - - .mx_IncomingCallBox_unSilence::before { - mask-image: url('$(res)/img/voip/un-silence.svg'); - } - } } diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index eff865f20c3..60b7aa69eab 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -39,7 +39,7 @@ limitations under the License. .mx_CallView_pip { width: 320px; padding-bottom: 8px; - background-color: $voipcall-plinth-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 6adc8214873..e4ea2bb57e6 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -115,8 +115,8 @@ $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -// this probably shouldn't have it's own colour -$voipcall-plinth-color: #394049; +$quinary-content-color: #394049; +$toast-bg-color: $quinary-content-color; // ******************** diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 0c0197cfb06..064b532bb0e 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,8 +111,8 @@ $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -// this probably shouldn't have it's own colour -$voipcall-plinth-color: #394049; +$quinary-content-color: #394049; +$toast-bg-color: $quinary-content-color; // ******************** diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index e9bfe46c894..e945efb219c 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -181,7 +181,7 @@ $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; -// this probably shouldn't have it's own colour +$toast-bg-color: $system-light; $voipcall-plinth-color: $system-light; // ******************** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 90ae808adca..aa17dddc561 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -170,7 +170,7 @@ $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; -// this probably shouldn't have it's own colour +$toast-bg-color: $system-light; $voipcall-plinth-color: $system-light; // ******************** diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e7c1dda54f3..caf010200a3 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -86,6 +86,9 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/ import EventEmitter from 'events'; import SdkConfig from './SdkConfig'; import { ensureDMExists, findDMForUser } from './createRoom'; +import { getIncomingCallToastKey } from './toasts/IncomingCallToast'; +import ToastStore from './stores/ToastStore'; +import IncomingCallToast from "./toasts/IncomingCallToast"; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -624,6 +627,19 @@ export default class CallHandler extends EventEmitter { `Call state in ${mappedRoomId} changed to ${status}`, ); + const toastKey = getIncomingCallToastKey(call.callId); + if (status === CallState.Ringing) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey, + priority: 100, + component: IncomingCallToast, + bodyClassName: "mx_IncomingCallToast", + props: { call }, + }); + } else { + ToastStore.sharedInstance().dismissToast(toastKey); + } + dis.dispatch({ action: 'call_state', room_id: mappedRoomId, diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index b7b0b7c6521..0b0e8719754 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -58,28 +58,39 @@ export default class ToastContainer extends React.Component<{}, IState> { let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const { title, icon, key, component, className, props } = topToast; - const toastClasses = classNames("mx_Toast_toast", { + const { title, icon, key, component, className, bodyClassName, props } = topToast; + const bodyClasses = classNames("mx_Toast_body", bodyClassName); + const toastClasses = classNames("mx_Toast_toast", className, { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, - }, className); + }); + const toastProps = Object.assign({}, props, { + key, + toastKey: key, + }); + const content = React.createElement(component, toastProps); let countIndicator; - if (isStacked || this.state.countSeen > 0) { + if (title && isStacked || this.state.countSeen > 0) { countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; } - const toastProps = Object.assign({}, props, { - key, - toastKey: key, - }); - toast = (
-
-

{ title }

- { countIndicator } + let titleElement; + if (title) { + titleElement = ( +
+

{ title }

+ { countIndicator } +
+ ); + } + + toast = ( +
+ { titleElement } +
{ content }
-
{ React.createElement(component, toastProps) }
-
); + ); containerClasses = classNames("mx_ToastContainer", { "mx_ToastContainer_stacked": isStacked, diff --git a/src/components/views/voip/CallContainer.tsx b/src/components/views/voip/CallContainer.tsx index fa963e4e282..41046b99529 100644 --- a/src/components/views/voip/CallContainer.tsx +++ b/src/components/views/voip/CallContainer.tsx @@ -1,5 +1,6 @@ /* Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import IncomingCallBox from './IncomingCallBox'; import CallPreview from './CallPreview'; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -31,7 +31,6 @@ interface IState { export default class CallContainer extends React.PureComponent { public render() { return
-
; } diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx deleted file mode 100644 index 95e97f10803..00000000000 --- a/src/components/views/voip/IncomingCallBox.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2019, 2020 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 React from 'react'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import dis from '../../../dispatcher/dispatcher'; -import { _t } from '../../../languageHandler'; -import { ActionPayload } from '../../../dispatcher/payloads'; -import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; -import RoomAvatar from '../avatars/RoomAvatar'; -import AccessibleButton from '../elements/AccessibleButton'; -import { CallState } from 'matrix-js-sdk/src/webrtc/call'; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; -import classNames from 'classnames'; - -interface IProps { -} - -interface IState { - incomingCall: any; - silenced: boolean; -} - -@replaceableComponent("views.voip.IncomingCallBox") -export default class IncomingCallBox extends React.Component { - private dispatcherRef: string; - - constructor(props: IProps) { - super(props); - - this.dispatcherRef = dis.register(this.onAction); - this.state = { - incomingCall: null, - silenced: false, - }; - } - - componentDidMount = () => { - CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); - }; - - public componentWillUnmount() { - dis.unregister(this.dispatcherRef); - CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); - } - - private onAction = (payload: ActionPayload) => { - switch (payload.action) { - case 'call_state': { - const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id); - if (call && call.state === CallState.Ringing) { - this.setState({ - incomingCall: call, - silenced: false, // Reset silenced state for new call - }); - } else { - this.setState({ - incomingCall: null, - }); - } - } - } - }; - - private onSilencedCallsChanged = () => { - const callId = this.state.incomingCall?.callId; - if (!callId) return; - this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(callId) }); - }; - - private onAnswerClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - dis.dispatch({ - action: 'answer', - room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), - }); - }; - - private onRejectClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - dis.dispatch({ - action: 'reject', - room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), - }); - }; - - private onSilenceClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - const callId = this.state.incomingCall.callId; - this.state.silenced ? - CallHandler.sharedInstance().unSilenceCall(callId): - CallHandler.sharedInstance().silenceCall(callId); - }; - - public render() { - if (!this.state.incomingCall) { - return null; - } - - let room = null; - if (this.state.incomingCall) { - room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall)); - } - - const caller = room ? room.name : _t("Unknown caller"); - - let incomingCallText = null; - if (this.state.incomingCall) { - if (this.state.incomingCall.type === "voice") { - incomingCallText = _t("Incoming voice call"); - } else if (this.state.incomingCall.type === "video") { - incomingCallText = _t("Incoming video call"); - } else { - incomingCallText = _t("Incoming call"); - } - } - - const silenceClass = classNames({ - "mx_IncomingCallBox_iconButton": true, - "mx_IncomingCallBox_unSilence": this.state.silenced, - "mx_IncomingCallBox_silence": !this.state.silenced, - }); - - return
-
- -
-

{ caller }

-

{ incomingCallText }

-
- -
-
- - { _t("Decline") } - -
- - { _t("Accept") } - -
-
; - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3ad8daa85cf..6867f28b746 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -734,6 +734,13 @@ "Notifications": "Notifications", "Enable desktop notifications": "Enable desktop notifications", "Enable": "Enable", + "Unknown caller": "Unknown caller", + "Voice call": "Voice call", + "Video call": "Video call", + "Decline": "Decline", + "Accept": "Accept", + "Sound on": "Sound on", + "Silence call": "Silence call", "Use app for a better experience": "Use app for a better experience", "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.", "Use app": "Use app", @@ -912,14 +919,6 @@ "Fill Screen": "Fill Screen", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", - "Unknown caller": "Unknown caller", - "Incoming voice call": "Incoming voice call", - "Incoming video call": "Incoming video call", - "Incoming call": "Incoming call", - "Sound on": "Sound on", - "Silence call": "Silence call", - "Decline": "Decline", - "Accept": "Accept", "The other party cancelled the verification.": "The other party cancelled the verification.", "Verified!": "Verified!", "You've successfully verified this user.": "You've successfully verified this user.", @@ -1582,8 +1581,6 @@ "Hide Widgets": "Hide Widgets", "Show Widgets": "Show Widgets", "Search": "Search", - "Voice call": "Voice call", - "Video call": "Video call", "Invites": "Invites", "Favourites": "Favourites", "People": "People", diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 850c3cb026d..5e51de3e262 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -22,10 +22,11 @@ export interface IToast { key: string; // higher priority number will be shown on top of lower priority priority: number; - title: string; + title?: string; icon?: string; component: C; className?: string; + bodyClassName?: string; props?: Omit, "toastKey">; // toastKey is injected by ToastContainer } diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx new file mode 100644 index 00000000000..a853e1652ab --- /dev/null +++ b/src/toasts/IncomingCallToast.tsx @@ -0,0 +1,140 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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 React from 'react'; +import { CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import classNames from 'classnames'; +import { replaceableComponent } from '../utils/replaceableComponent'; +import CallHandler, { CallHandlerEvent } from '../CallHandler'; +import dis from '../dispatcher/dispatcher'; +import { MatrixClientPeg } from '../MatrixClientPeg'; +import { _t } from '../languageHandler'; +import RoomAvatar from '../components/views/avatars/RoomAvatar'; +import AccessibleTooltipButton from '../components/views/elements/AccessibleTooltipButton'; +import AccessibleButton from '../components/views/elements/AccessibleButton'; + +export const getIncomingCallToastKey = (callId: string) => `call_${callId}`; + +interface IProps { + call: MatrixCall; +} + +interface IState { + silenced: boolean; +} + +@replaceableComponent("views.voip.IncomingCallToast") +export default class IncomingCallToast extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + silenced: false, + }; + } + + public componentDidMount = (): void => { + CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); + }; + + public componentWillUnmount(): void { + CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); + } + + private onSilencedCallsChanged = (): void => { + this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId) }); + }; + + private onAnswerClick= (e: React.MouseEvent): void => { + e.stopPropagation(); + dis.dispatch({ + action: 'answer', + room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call), + }); + }; + + private onRejectClick= (e: React.MouseEvent): void => { + e.stopPropagation(); + dis.dispatch({ + action: 'reject', + room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call), + }); + }; + + private onSilenceClick = (e: React.MouseEvent): void => { + e.stopPropagation(); + const callId = this.props.call.callId; + this.state.silenced ? + CallHandler.sharedInstance().unSilenceCall(callId) : + CallHandler.sharedInstance().silenceCall(callId); + }; + + public render() { + const call = this.props.call; + const room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); + const isVoice = call.type === CallType.Voice; + + const contentClass = classNames("mx_IncomingCallToast_content", { + "mx_IncomingCallToast_content_voice": isVoice, + "mx_IncomingCallToast_content_video": !isVoice, + }); + const silenceClass = classNames("mx_IncomingCallToast_iconButton", { + "mx_IncomingCallToast_unSilence": this.state.silenced, + "mx_IncomingCallToast_silence": !this.state.silenced, + }); + + return + +
+ + { room ? room.name : _t("Unknown caller") } + +
+
+ { isVoice ? _t("Voice call") : _t("Video call") } +
+
+ + { _t("Decline") } + + + { _t("Accept") } + +
+
+ + ; + } +}