mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-11-04 02:02:14 +01:00 
			
		
		
		
	Merge pull request #6470 from SimonBrandner/feature/incoming-call-toast
This commit is contained in:
		
						commit
						94e77e70c6
					
				@ -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";
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 {
 | 
			
		||||
        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 {
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
// ********************
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
// ********************
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
// ********************
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
// ********************
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
            let countIndicator;
 | 
			
		||||
            if (isStacked || this.state.countSeen > 0) {
 | 
			
		||||
                countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
            const toastProps = Object.assign({}, props, {
 | 
			
		||||
                key,
 | 
			
		||||
                toastKey: key,
 | 
			
		||||
            });
 | 
			
		||||
            toast = (<div className={toastClasses}>
 | 
			
		||||
            const content = React.createElement(component, toastProps);
 | 
			
		||||
 | 
			
		||||
            let countIndicator;
 | 
			
		||||
            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>
 | 
			
		||||
                <div className="mx_Toast_body">{ React.createElement(component, toastProps) }</div>
 | 
			
		||||
            </div>);
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            toast = (
 | 
			
		||||
                <div className={toastClasses}>
 | 
			
		||||
                    { titleElement }
 | 
			
		||||
                    <div className={bodyClasses}>{ content }</div>
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            containerClasses = classNames("mx_ToastContainer", {
 | 
			
		||||
                "mx_ToastContainer_stacked": isStacked,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
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.
 | 
			
		||||
@ -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<IProps, IState> {
 | 
			
		||||
    public render() {
 | 
			
		||||
        return <div className="mx_CallContainer">
 | 
			
		||||
            <IncomingCallBox />
 | 
			
		||||
            <CallPreview />
 | 
			
		||||
        </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",
 | 
			
		||||
    "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",
 | 
			
		||||
 | 
			
		||||
@ -22,10 +22,11 @@ export interface IToast<C extends ComponentClass> {
 | 
			
		||||
    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<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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user