;
- }
-
// if we have search results, we keep the messagepanel (so that it preserves its
// scroll state), but hide it.
let searchResultsPanel;
diff --git a/src/components/views/avatars/PulsedAvatar.tsx b/src/components/views/avatars/PulsedAvatar.tsx
deleted file mode 100644
index b4e876b9f6..0000000000
--- a/src/components/views/avatars/PulsedAvatar.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
-Copyright 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';
-
-interface IProps {
-}
-
-const PulsedAvatar: React.FC = (props) => {
- return
- {props.children}
-
;
-};
-
-export default PulsedAvatar;
diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx
index 3d9235792b..8e1b0dd963 100644
--- a/src/components/views/voip/CallPreview.tsx
+++ b/src/components/views/voip/CallPreview.tsx
@@ -26,6 +26,15 @@ import PersistentApp from "../elements/PersistentApp";
import SettingsStore from "../../../settings/SettingsStore";
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
+const SHOW_CALL_IN_STATES = [
+ CallState.Connected,
+ CallState.InviteSent,
+ CallState.Connecting,
+ CallState.CreateAnswer,
+ CallState.CreateOffer,
+ CallState.WaitLocalMedia,
+];
+
interface IProps {
}
@@ -94,14 +103,13 @@ export default class CallPreview extends React.Component {
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
const showCall = (
this.state.activeCall &&
- this.state.activeCall.state === CallState.Connected &&
+ SHOW_CALL_IN_STATES.includes(this.state.activeCall.state) &&
!callForRoom
);
if (showCall) {
return (
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 653a72cca0..db6d2b7ae0 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -21,12 +21,13 @@ import dis from '../../../dispatcher/dispatcher';
import CallHandler from '../../../CallHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
-import AccessibleButton from '../elements/AccessibleButton';
import VideoFeed, { VideoFeedType } from "./VideoFeed";
import RoomAvatar from "../avatars/RoomAvatar";
-import PulsedAvatar from '../avatars/PulsedAvatar';
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
+import classNames from 'classnames';
+import AccessibleButton from '../elements/AccessibleButton';
+import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
interface IProps {
// js-sdk room object. If set, we will only show calls for the given
@@ -43,9 +44,6 @@ interface IProps {
// in a way that is likely to cause a resize.
onResize?: any;
- // classname applied to view,
- className?: string;
-
// Whether to show the hang up icon:W
showHangup?: boolean;
}
@@ -53,6 +51,10 @@ interface IProps {
interface IState {
call: MatrixCall;
isLocalOnHold: boolean,
+ micMuted: boolean,
+ vidMuted: boolean,
+ callState: CallState,
+ controlsVisible: boolean,
}
function getFullScreenElement() {
@@ -83,10 +85,15 @@ function exitFullscreen() {
if (exitMethod) exitMethod.call(document);
}
+const CONTROLS_HIDE_DELAY = 1000;
+// Height of the header duplicated from CSS because we need to subtract it from our max
+// height to get the max height of the video
+const HEADER_HEIGHT = 44;
+
export default class CallView extends React.Component {
private dispatcherRef: string;
- private container = createRef();
-
+ private contentRef = createRef();
+ private controlsHideTimer: number = null;
constructor(props: IProps) {
super(props);
@@ -94,6 +101,10 @@ export default class CallView extends React.Component {
this.state = {
call,
isLocalOnHold: call ? call.isLocalOnHold() : null,
+ micMuted: call ? call.isMicrophoneMuted() : null,
+ vidMuted: call ? call.isLocalVideoMuted() : null,
+ callState: call ? call.state : null,
+ controlsVisible: true,
}
this.updateCallListeners(null, call);
@@ -101,9 +112,11 @@ export default class CallView extends React.Component {
public componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
+ document.addEventListener('keydown', this.onNativeKeyDown);
}
public componentWillUnmount() {
+ document.removeEventListener("keydown", this.onNativeKeyDown);
this.updateCallListeners(this.state.call, null);
dis.unregister(this.dispatcherRef);
}
@@ -111,11 +124,11 @@ export default class CallView extends React.Component {
private onAction = (payload) => {
switch (payload.action) {
case 'video_fullscreen': {
- if (!this.container.current) {
+ if (!this.contentRef.current) {
return;
}
if (payload.fullscreen) {
- requestFullscreen(this.container.current);
+ requestFullscreen(this.contentRef.current);
} else if (getFullScreenElement()) {
exitFullscreen();
}
@@ -125,9 +138,21 @@ export default class CallView extends React.Component {
const newCall = this.getCall();
if (newCall !== this.state.call) {
this.updateCallListeners(this.state.call, newCall);
+ let newControlsVisible = this.state.controlsVisible;
+ if (newCall && !this.state.call) {
+ newControlsVisible = true;
+ if (this.controlsHideTimer !== null) {
+ clearTimeout(this.controlsHideTimer);
+ }
+ this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
+ }
this.setState({
call: newCall,
isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
+ micMuted: newCall ? newCall.isMicrophoneMuted() : null,
+ vidMuted: newCall ? newCall.isLocalVideoMuted() : null,
+ callState: newCall ? newCall.state : null,
+ controlsVisible: newControlsVisible,
});
}
if (!newCall && getFullScreenElement()) {
@@ -144,11 +169,6 @@ export default class CallView extends React.Component {
if (this.props.room) {
const roomId = this.props.room.roomId;
call = CallHandler.sharedInstance().getCallForRoom(roomId);
-
- // We don't currently show voice calls in this view when in the room:
- // they're represented in the room status bar at the bottom instead
- // (but this will all change with the new designs)
- if (call && call.type == CallType.Voice) call = null;
} else {
call = CallHandler.sharedInstance().getAnyActiveCall();
// Ignore calls if we can't get the room associated with them.
@@ -160,7 +180,7 @@ export default class CallView extends React.Component {
}
}
- if (call && call.state == CallState.Ended) return null;
+ if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
return call;
}
@@ -177,67 +197,240 @@ export default class CallView extends React.Component {
});
};
- public render() {
- let view: React.ReactNode;
+ private onFullscreenClick = () => {
+ dis.dispatch({
+ action: 'video_fullscreen',
+ fullscreen: true,
+ });
+ };
- if (this.state.call) {
- if (this.state.call.type === "voice") {
- const client = MatrixClientPeg.get();
- const callRoom = client.getRoom(this.state.call.roomId);
+ private onExpandClick = () => {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: this.state.call.roomId,
+ });
+ };
- let caption = _t("Active call");
- if (this.state.isLocalOnHold) {
- // we currently have no UI for holding / unholding a call (apart from slash
- // commands) so we don't disintguish between when we've put the call on hold
- // (ie. we'd show an unhold button) and when the other side has put us on hold
- // (where obviously we would not show such a button).
- caption = _t("Call Paused");
+ private onControlsHideTimer = () => {
+ this.controlsHideTimer = null;
+ this.setState({
+ controlsVisible: false,
+ });
+ }
+
+ private onMouseMove = () => {
+ this.showControls();
+ }
+
+ private showControls() {
+ if (!this.state.controlsVisible) {
+ this.setState({
+ controlsVisible: true,
+ });
+ }
+ if (this.controlsHideTimer !== null) {
+ clearTimeout(this.controlsHideTimer);
+ }
+ this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
+ }
+
+ private onMicMuteClick = () => {
+ if (!this.state.call) return;
+
+ const newVal = !this.state.micMuted;
+
+ this.state.call.setMicrophoneMuted(newVal);
+ this.setState({micMuted: newVal});
+ }
+
+ private onVidMuteClick = () => {
+ if (!this.state.call) return;
+
+ const newVal = !this.state.vidMuted;
+
+ this.state.call.setLocalVideoMuted(newVal);
+ this.setState({vidMuted: newVal});
+ }
+
+ // we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
+ // Note that this assumes we always have a callview on screen at any given time
+ // CallHandler would probably be a better place for this
+ private onNativeKeyDown = ev => {
+ let handled = false;
+ const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
+
+ switch (ev.key) {
+ case Key.D:
+ if (ctrlCmdOnly) {
+ this.onMicMuteClick();
+ // show the controls to give feedback
+ this.showControls();
+ handled = true;
}
+ break;
- view =
-
-
-
-
-
{callRoom.name}
-
{ caption }
-
- ;
- } else {
- // For video calls, we currently ignore the call hold state altogether
- // (the video will just go black)
-
- // if we're fullscreen, we don't want to set a maxHeight on the video element.
- const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight;
- view =
-
-
-
;
- }
+ case Key.E:
+ if (ctrlCmdOnly) {
+ this.onVidMuteClick();
+ // show the controls to give feedback
+ this.showControls();
+ handled = true;
+ }
+ break;
}
- let hangup: React.ReactNode;
- if (this.props.showHangup) {
- hangup =
;
+ }
+
+ // The 'content' for the call, ie. the videos for a video call and profile picture
+ // for voice calls (fills the bg)
+ let contentView: React.ReactNode;
+
+ if (this.state.call.type === CallType.Video) {
+ // if we're fullscreen, we don't want to set a maxHeight on the video element.
+ const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight - HEADER_HEIGHT;
+ contentView =
;
}
}
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index 355dff9ff6..0403a9eb75 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -22,7 +22,6 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { ActionPayload } from '../../../dispatcher/payloads';
import CallHandler from '../../../CallHandler';
-import PulsedAvatar from '../avatars/PulsedAvatar';
import RoomAvatar from '../avatars/RoomAvatar';
import FormButton from '../elements/FormButton';
import { CallState } from 'matrix-js-sdk/lib/webrtc/call';
@@ -108,13 +107,11 @@ export default class IncomingCallBox extends React.Component {
return
-
-
-
+
{caller}
{incomingCallText}
diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx
index 9dba9fa9c8..5fb71a6d69 100644
--- a/src/components/views/voip/VideoFeed.tsx
+++ b/src/components/views/voip/VideoFeed.tsx
@@ -73,8 +73,6 @@ export default class VideoFeed extends React.Component {
let videoStyle = {};
if (this.props.maxHeight) videoStyle = { maxHeight: this.props.maxHeight };
- return
-
-
;
+ return ;
}
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index eb6688008c..0d50128f32 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -836,8 +836,10 @@
"When rooms are upgraded": "When rooms are upgraded",
"My Ban List": "My Ban List",
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
- "Active call": "Active call",
- "Call Paused": "Call Paused",
+ "Video Call": "Video Call",
+ "Voice Call": "Voice Call",
+ "Fill Screen": "Fill Screen",
+ "Return to call": "Return to call",
"Unknown caller": "Unknown caller",
"Incoming voice call": "Incoming voice call",
"Incoming video call": "Incoming video call",
@@ -2439,10 +2441,6 @@
"%(count)s of your messages have not been sent.|one": "Your message was not sent.",
"%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Resend all or cancel all now. You can also select individual messages to resend or cancel.",
"%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "Resend message or cancel message now.",
- "Calling...": "Calling...",
- "Call connecting...": "Call connecting...",
- "Starting camera...": "Starting camera...",
- "Starting microphone...": "Starting microphone...",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
@@ -2454,11 +2452,6 @@
"Failed to reject invite": "Failed to reject invite",
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
- "Fill screen": "Fill screen",
- "Click to unmute video": "Click to unmute video",
- "Click to mute video": "Click to mute video",
- "Click to unmute audio": "Click to unmute audio",
- "Click to mute audio": "Click to mute audio",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position",