mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 01:35:49 +03:00
Merge pull request #6470 from SimonBrandner/feature/incoming-call-toast
This commit is contained in:
commit
94e77e70c6
16 changed files with 351 additions and 293 deletions
|
@ -266,6 +266,7 @@
|
||||||
@import "./views/spaces/_SpacePublicShare.scss";
|
@import "./views/spaces/_SpacePublicShare.scss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||||
@import "./views/toasts/_AnalyticsToast.scss";
|
@import "./views/toasts/_AnalyticsToast.scss";
|
||||||
|
@import "./views/toasts/_IncomingCallToast.scss";
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
|
|
|
@ -28,7 +28,7 @@ limitations under the License.
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
grid-row: 2 / 4;
|
grid-row: 2 / 4;
|
||||||
grid-column: 1;
|
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);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ limitations under the License.
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
color: $primary-fg-color;
|
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);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
149
res/css/views/toasts/_IncomingCallToast.scss
Normal file
149
res/css/views/toasts/_IncomingCallToast.scss
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,84 +43,4 @@ limitations under the License.
|
||||||
.mx_AppTile_persistedWrapper div {
|
.mx_AppTile_persistedWrapper div {
|
||||||
min-width: 350px;
|
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ limitations under the License.
|
||||||
.mx_CallView_pip {
|
.mx_CallView_pip {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
background-color: $voipcall-plinth-color;
|
background-color: $toast-bg-color;
|
||||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
|
|
@ -115,8 +115,8 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$header-divider-color: $header-panel-text-primary-color;
|
$header-divider-color: $header-panel-text-primary-color;
|
||||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
$quinary-content-color: #394049;
|
||||||
$voipcall-plinth-color: #394049;
|
$toast-bg-color: $quinary-content-color;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
|
|
@ -111,8 +111,8 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$header-divider-color: $header-panel-text-primary-color;
|
$header-divider-color: $header-panel-text-primary-color;
|
||||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// this probably shouldn't have it's own colour
|
$quinary-content-color: #394049;
|
||||||
$voipcall-plinth-color: #394049;
|
$toast-bg-color: $quinary-content-color;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
|
|
@ -181,7 +181,7 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$composer-e2e-icon-color: #91a1c0;
|
$composer-e2e-icon-color: #91a1c0;
|
||||||
$header-divider-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;
|
$voipcall-plinth-color: $system-light;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
|
@ -170,7 +170,7 @@ $eventtile-meta-color: $roomtopic-color;
|
||||||
$composer-e2e-icon-color: #91A1C0;
|
$composer-e2e-icon-color: #91A1C0;
|
||||||
$header-divider-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;
|
$voipcall-plinth-color: $system-light;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
|
@ -86,6 +86,9 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
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 = 'm.protocol.pstn';
|
||||||
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.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}`,
|
`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({
|
dis.dispatch({
|
||||||
action: 'call_state',
|
action: 'call_state',
|
||||||
room_id: mappedRoomId,
|
room_id: mappedRoomId,
|
||||||
|
|
|
@ -58,28 +58,39 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
let containerClasses;
|
let containerClasses;
|
||||||
if (totalCount !== 0) {
|
if (totalCount !== 0) {
|
||||||
const topToast = this.state.toasts[0];
|
const topToast = this.state.toasts[0];
|
||||||
const { title, icon, key, component, className, props } = topToast;
|
const { title, icon, key, component, className, bodyClassName, props } = topToast;
|
||||||
const toastClasses = classNames("mx_Toast_toast", {
|
const bodyClasses = classNames("mx_Toast_body", bodyClassName);
|
||||||
|
const toastClasses = classNames("mx_Toast_toast", className, {
|
||||||
"mx_Toast_hasIcon": icon,
|
"mx_Toast_hasIcon": icon,
|
||||||
[`mx_Toast_icon_${icon}`]: icon,
|
[`mx_Toast_icon_${icon}`]: icon,
|
||||||
}, className);
|
});
|
||||||
|
|
||||||
let countIndicator;
|
|
||||||
if (isStacked || this.state.countSeen > 0) {
|
|
||||||
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastProps = Object.assign({}, props, {
|
const toastProps = Object.assign({}, props, {
|
||||||
key,
|
key,
|
||||||
toastKey: key,
|
toastKey: key,
|
||||||
});
|
});
|
||||||
toast = (<div className={toastClasses}>
|
const content = React.createElement(component, toastProps);
|
||||||
<div className="mx_Toast_title">
|
|
||||||
<h2>{ title }</h2>
|
let countIndicator;
|
||||||
<span>{ countIndicator }</span>
|
if (title && isStacked || this.state.countSeen > 0) {
|
||||||
|
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleElement;
|
||||||
|
if (title) {
|
||||||
|
titleElement = (
|
||||||
|
<div className="mx_Toast_title">
|
||||||
|
<h2>{ title }</h2>
|
||||||
|
<span>{ countIndicator }</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast = (
|
||||||
|
<div className={toastClasses}>
|
||||||
|
{ titleElement }
|
||||||
|
<div className={bodyClasses}>{ content }</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Toast_body">{ React.createElement(component, toastProps) }</div>
|
);
|
||||||
</div>);
|
|
||||||
|
|
||||||
containerClasses = classNames("mx_ToastContainer", {
|
containerClasses = classNames("mx_ToastContainer", {
|
||||||
"mx_ToastContainer_stacked": isStacked,
|
"mx_ToastContainer_stacked": isStacked,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 React from 'react';
|
||||||
import IncomingCallBox from './IncomingCallBox';
|
|
||||||
import CallPreview from './CallPreview';
|
import CallPreview from './CallPreview';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ interface IState {
|
||||||
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
return <div className="mx_CallContainer">
|
return <div className="mx_CallContainer">
|
||||||
<IncomingCallBox />
|
|
||||||
<CallPreview />
|
<CallPreview />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<IProps, IState> {
|
|
||||||
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 <div className="mx_IncomingCallBox">
|
|
||||||
<div className="mx_IncomingCallBox_CallerInfo">
|
|
||||||
<RoomAvatar
|
|
||||||
room={room}
|
|
||||||
height={32}
|
|
||||||
width={32}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h1>{ caller }</h1>
|
|
||||||
<p>{ incomingCallText }</p>
|
|
||||||
</div>
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className={silenceClass}
|
|
||||||
onClick={this.onSilenceClick}
|
|
||||||
title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx_IncomingCallBox_buttons">
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_IncomingCallBox_decline"
|
|
||||||
onClick={this.onRejectClick}
|
|
||||||
kind="danger"
|
|
||||||
>
|
|
||||||
{ _t("Decline") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<div className="mx_IncomingCallBox_spacer" />
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_IncomingCallBox_accept"
|
|
||||||
onClick={this.onAnswerClick}
|
|
||||||
kind="primary"
|
|
||||||
>
|
|
||||||
{ _t("Accept") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -734,6 +734,13 @@
|
||||||
"Notifications": "Notifications",
|
"Notifications": "Notifications",
|
||||||
"Enable desktop notifications": "Enable desktop notifications",
|
"Enable desktop notifications": "Enable desktop notifications",
|
||||||
"Enable": "Enable",
|
"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",
|
"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.",
|
"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",
|
"Use app": "Use app",
|
||||||
|
@ -912,14 +919,6 @@
|
||||||
"Fill Screen": "Fill Screen",
|
"Fill Screen": "Fill Screen",
|
||||||
"Return to call": "Return to call",
|
"Return to call": "Return to call",
|
||||||
"%(name)s on hold": "%(name)s on hold",
|
"%(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.",
|
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
||||||
"Verified!": "Verified!",
|
"Verified!": "Verified!",
|
||||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||||
|
@ -1582,8 +1581,6 @@
|
||||||
"Hide Widgets": "Hide Widgets",
|
"Hide Widgets": "Hide Widgets",
|
||||||
"Show Widgets": "Show Widgets",
|
"Show Widgets": "Show Widgets",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Voice call": "Voice call",
|
|
||||||
"Video call": "Video call",
|
|
||||||
"Invites": "Invites",
|
"Invites": "Invites",
|
||||||
"Favourites": "Favourites",
|
"Favourites": "Favourites",
|
||||||
"People": "People",
|
"People": "People",
|
||||||
|
|
|
@ -22,10 +22,11 @@ export interface IToast<C extends ComponentClass> {
|
||||||
key: string;
|
key: string;
|
||||||
// higher priority number will be shown on top of lower priority
|
// higher priority number will be shown on top of lower priority
|
||||||
priority: number;
|
priority: number;
|
||||||
title: string;
|
title?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: C;
|
component: C;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
bodyClassName?: string;
|
||||||
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
|
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
140
src/toasts/IncomingCallToast.tsx
Normal file
140
src/toasts/IncomingCallToast.tsx
Normal file
|
@ -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 <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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<IProps, IState> {
|
||||||
|
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 <React.Fragment>
|
||||||
|
<RoomAvatar
|
||||||
|
room={room}
|
||||||
|
height={32}
|
||||||
|
width={32}
|
||||||
|
/>
|
||||||
|
<div className={contentClass}>
|
||||||
|
<span className="mx_CallEvent_caller">
|
||||||
|
{ room ? room.name : _t("Unknown caller") }
|
||||||
|
</span>
|
||||||
|
<div className="mx_CallEvent_type">
|
||||||
|
<div className="mx_CallEvent_type_icon" />
|
||||||
|
{ isVoice ? _t("Voice call") : _t("Video call") }
|
||||||
|
</div>
|
||||||
|
<div className="mx_IncomingCallToast_buttons">
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_decline"
|
||||||
|
onClick={this.onRejectClick}
|
||||||
|
kind="danger"
|
||||||
|
>
|
||||||
|
<span> { _t("Decline") } </span>
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_accept"
|
||||||
|
onClick={this.onAnswerClick}
|
||||||
|
kind="primary"
|
||||||
|
>
|
||||||
|
<span> { _t("Accept") } </span>
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={silenceClass}
|
||||||
|
onClick={this.onSilenceClick}
|
||||||
|
title={this.state.silenced ? _t("Sound on") : _t("Silence call")}
|
||||||
|
/>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue