mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-10-31 00:01:23 +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}> | ||||
|                 <div className="mx_Toast_title"> | ||||
|                     <h2>{ title }</h2> | ||||
|                     <span>{ countIndicator }</span> | ||||
|             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> | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             toast = ( | ||||
|                 <div className={toastClasses}> | ||||
|                     { titleElement } | ||||
|                     <div className={bodyClasses}>{ content }</div> | ||||
|                 </div> | ||||
|                 <div className="mx_Toast_body">{ React.createElement(component, toastProps) }</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