implement the 'correct' voip calling design

This commit is contained in:
Matthew Hodgson 2015-12-14 23:37:34 +00:00
parent e81a401bba
commit 39c628d4a1
3 changed files with 134 additions and 146 deletions

View file

@ -59,6 +59,7 @@ module.exports = React.createClass({
searchResults: null, searchResults: null,
syncState: MatrixClientPeg.get().getSyncState(), syncState: MatrixClientPeg.get().getSyncState(),
hasUnsentMessages: this._hasUnsentMessages(room), hasUnsentMessages: this._hasUnsentMessages(room),
callState: null,
} }
}, },
@ -114,7 +115,17 @@ module.exports = React.createClass({
this.forceUpdate(); this.forceUpdate();
break; break;
case 'call_state': case 'call_state':
if (CallHandler.getCallForRoom(this.props.roomId)) { // don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (!payload.room_id) {
return;
}
var call = CallHandler.getCallForRoom(payload.room_id);
var callState;
if (call) {
// Call state has changed so we may be loading video elements // Call state has changed so we may be loading video elements
// which will obscure the message log. // which will obscure the message log.
// scroll to bottom // scroll to bottom
@ -122,11 +133,20 @@ module.exports = React.createClass({
if (scrollNode) { if (scrollNode) {
scrollNode.scrollTop = scrollNode.scrollHeight; scrollNode.scrollTop = scrollNode.scrollHeight;
} }
callState = call.call_state;
}
else {
callState = "ended";
} }
// possibly remove the conf call notification if we're now in // possibly remove the conf call notification if we're now in
// the conf // the conf
this._updateConfCallNotification(); this._updateConfCallNotification();
this.setState({
callState: callState
});
break; break;
case 'user_activity': case 'user_activity':
this.sendReadReceipt(); this.sendReadReceipt();
@ -276,6 +296,12 @@ module.exports = React.createClass({
this.fillSpace(); this.fillSpace();
} }
var call = CallHandler.getCallForRoom(this.props.roomId);
var callState = call ? call.call_state : "ended";
this.setState({
callState: callState
});
this._updateConfCallNotification(); this._updateConfCallNotification();
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
@ -972,6 +998,30 @@ module.exports = React.createClass({
} }
}, },
onMuteAudioClick: function() {
var call = CallHandler.getCallForRoom(this.props.roomId);
if (!call) {
return;
}
var newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState);
this.setState({
audioMuted: newState
});
},
onMuteVideoClick: function() {
var call = CallHandler.getCallForRoom(this.props.roomId);
if (!call) {
return;
}
var newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState);
this.setState({
videoMuted: newState
});
},
render: function() { render: function() {
var RoomHeader = sdk.getComponent('rooms.RoomHeader'); var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var MessageComposer = sdk.getComponent('rooms.MessageComposer'); var MessageComposer = sdk.getComponent('rooms.MessageComposer');
@ -1029,9 +1079,7 @@ module.exports = React.createClass({
loading: this.state.paginating loading: this.state.paginating
}); });
var statusBar = ( var statusBar;
<div />
);
// for testing UI... // for testing UI...
// this.state.upload = { // this.state.upload = {
@ -1043,7 +1091,7 @@ module.exports = React.createClass({
if (ContentMessages.getCurrentUploads().length > 0) { if (ContentMessages.getCurrentUploads().length > 0) {
var UploadBar = sdk.getComponent('structures.UploadBar'); var UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} /> statusBar = <UploadBar room={this.state.room} />
} else { } else if (!this.state.searchResults) {
var typingString = this.getWhoIsTypingString(); var typingString = this.getWhoIsTypingString();
// typingString = "S͚͍̭̪̤͙̱͙̖̥͙̥̤̻̙͕͓͂̌ͬ͐̂k̜̝͎̰̥̻̼̂̌͛͗͊̅̒͂̊̍̍͌̈̈́͌̋̊ͬa͉̯͚̺̗̳̩ͪ̋̑͌̓̆̍̂̉̏̅̆ͧ̌̑v̲̲̪̝ͥ̌ͨͮͭ̊͆̾ͮ̍ͮ͑̚e̮̙͈̱̘͕̼̮͒ͩͨͫ̃͗̇ͩ͒ͣͦ͒̄̍͐ͣ̿ͥṘ̗̺͇̺̺͔̄́̊̓͊̍̃ͨ̚ā̼͎̘̟̼͎̜̪̪͚̋ͨͨͧ̓ͦͯͤ̄͆̋͂ͩ͌ͧͅt̙̙̹̗̦͖̞ͫͪ͑̑̅ͪ̃̚ͅ is typing..."; // typingString = "S͚͍̭̪̤͙̱͙̖̥͙̥̤̻̙͕͓͂̌ͬ͐̂k̜̝͎̰̥̻̼̂̌͛͗͊̅̒͂̊̍̍͌̈̈́͌̋̊ͬa͉̯͚̺̗̳̩ͪ̋̑͌̓̆̍̂̉̏̅̆ͧ̌̑v̲̲̪̝ͥ̌ͨͮͭ̊͆̾ͮ̍ͮ͑̚e̮̙͈̱̘͕̼̮͒ͩͨͫ̃͗̇ͩ͒ͣͦ͒̄̍͐ͣ̿ͥṘ̗̺͇̺̺͔̄́̊̓͊̍̃ͨ̚ā̼͎̘̟̼͎̜̪̪͚̋ͨͨͧ̓ͦͯͤ̄͆̋͂ͩ͌ͧͅt̙̙̹̗̦͖̞ͫͪ͑̑̅ͪ̃̚ͅ is typing...";
var unreadMsgs = this.getUnreadMessagesString(); var unreadMsgs = this.getUnreadMessagesString();
@ -1142,7 +1190,7 @@ module.exports = React.createClass({
var messageComposer, searchInfo; var messageComposer, searchInfo;
if (!this.state.searchResults) { if (!this.state.searchResults) {
messageComposer = messageComposer =
<MessageComposer room={this.state.room} roomView={this} uploadFile={this.uploadFile} /> <MessageComposer room={this.state.room} roomView={this} uploadFile={this.uploadFile} callState={this.state.callState} />
} }
else { else {
searchInfo = { searchInfo = {
@ -1152,8 +1200,43 @@ module.exports = React.createClass({
} }
} }
var call = CallHandler.getCallForRoom(this.props.roomId);
var inCall = false;
if (call && this.state.callState != 'ended') {
inCall = true;
//var muteVideoButton;
var voiceMuteButton, videoMuteButton;
if (call.type === "video") {
videoMuteButton =
<div className="mx_RoomView_muteButton" onClick={this.onMuteVideoClick}>
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"} width="31" height="27"/>
</div>
}
voiceMuteButton =
<div className="mx_RoomView_muteButton" onClick={this.onMuteAudioClick}>
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"} width="21" height="26"/>
</div>
if (!statusBar) {
statusBar =
<div className="mx_RoomView_callBar">
<img src="img/sound-indicator.svg" width="23" height="20" alt=""/>
<b>Active call</b>
</div>;
}
statusBar =
<div className="mx_RoomView_callStatusBar">
{ voiceMuteButton }
{ videoMuteButton }
{ statusBar }
<img className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17"/>
</div>
}
return ( return (
<div className="mx_RoomView"> <div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") }>
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo} editing={this.state.editingRoomSettings} onSearchClick={this.onSearchClick} <RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo} editing={this.state.editingRoomSettings} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} onLeaveClick={this.onLeaveClick} /> onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} onLeaveClick={this.onLeaveClick} />
{ fileDropTarget } { fileDropTarget }
@ -1174,7 +1257,7 @@ module.exports = React.createClass({
<div className="mx_RoomView_statusArea"> <div className="mx_RoomView_statusArea">
<div className="mx_RoomView_statusAreaBox"> <div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div> <div className="mx_RoomView_statusAreaBox_line"></div>
{ this.state.searchResults ? null : statusBar } { statusBar }
</div> </div>
</div> </div>
{ messageComposer } { messageComposer }

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require("react"); var React = require("react");
var marked = require("marked"); var marked = require("marked");
marked.setOptions({ marked.setOptions({
renderer: new marked.Renderer(), renderer: new marked.Renderer(),
@ -25,9 +26,11 @@ marked.setOptions({
smartLists: true, smartLists: true,
smartypants: false smartypants: false
}); });
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var SlashCommands = require("../../../SlashCommands"); var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var CallHandler = require('../../../CallHandler');
var sdk = require('../../../index'); var sdk = require('../../../index');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
@ -524,6 +527,19 @@ module.exports = React.createClass({
this.refs.uploadInput.value = null; this.refs.uploadInput.value = null;
}, },
onHangupClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
dis.dispatch({
action: 'hangup',
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId
});
},
onCallClick: function(ev) { onCallClick: function(ev) {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -544,6 +560,26 @@ module.exports = React.createClass({
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'}; var uploadInputStyle = {display: 'none'};
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var callButton, videoCallButton, hangupButton;
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (this.props.callState && this.props.callState !== 'ended') {
hangupButton =
<div className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<img src="img/hangup.svg" alt="Hangup" title="Hangup" width="25" height="26"/>
</div>;
}
else {
callButton =
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick}>
<img src="img/voice.svg" alt="Voice call" title="Voice call" width="16" height="26"/>
</div>
videoCallButton =
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick}>
<img src="img/call.svg" alt="Video call" title="Video call" width="30" height="22"/>
</div>
}
return ( return (
<div className="mx_MessageComposer"> <div className="mx_MessageComposer">
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
@ -558,12 +594,9 @@ module.exports = React.createClass({
<img src="img/upload.svg" alt="Upload file" title="Upload file" width="19" height="24"/> <img src="img/upload.svg" alt="Upload file" title="Upload file" width="19" height="24"/>
<input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} /> <input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
</div> </div>
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick}> { hangupButton }
<img src="img/voice.svg" alt="Voice call" title="Voice call" width="16" height="26"/> { callButton }
</div> { videoCallButton }
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick}>
<img src="img/call.svg" alt="Video call" title="Video call" width="30" height="22"/>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -16,15 +16,9 @@ limitations under the License.
'use strict'; 'use strict';
/*
* State vars:
* this.state.call_state = the UI state of the call (see CallHandler)
*/
var React = require('react'); var React = require('react');
var sdk = require('../../../index'); var sdk = require('../../../index');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
module.exports = React.createClass({ module.exports = React.createClass({
@ -47,34 +41,6 @@ module.exports = React.createClass({
}; };
}, },
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
if (this.props.room) {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
var callState = call ? call.call_state : "ended";
this.setState({
call_state: callState
});
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (payload.action !== 'call_state' || !payload.room_id) {
return;
}
var call = CallHandler.getCallForRoom(payload.room_id);
var callState = call ? call.call_state : "ended";
this.setState({
call_state: callState
});
},
onVideoClick: function(e) { onVideoClick: function(e) {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -82,6 +48,7 @@ module.exports = React.createClass({
room_id: this.props.room.roomId room_id: this.props.room.roomId
}); });
}, },
onVoiceClick: function() { onVoiceClick: function() {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -89,38 +56,6 @@ module.exports = React.createClass({
room_id: this.props.room.roomId room_id: this.props.room.roomId
}); });
}, },
onHangupClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) { return; }
dis.dispatch({
action: 'hangup',
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId
});
},
onMuteAudioClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
var newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState);
this.setState({
audioMuted: newState
});
},
onMuteVideoClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
var newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState);
this.setState({
videoMuted: newState
});
},
onNameChange: function(new_name) { onNameChange: function(new_name) {
if (this.props.room.name != new_name && new_name) { if (this.props.room.name != new_name && new_name) {
@ -152,42 +87,6 @@ module.exports = React.createClass({
else { else {
var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
var call_buttons;
if (this.state && this.state.call_state != 'ended') {
//var muteVideoButton;
var activeCall = (
CallHandler.getCallForRoom(this.props.room.roomId)
);
/*
if (activeCall && activeCall.type === "video") {
muteVideoButton = (
<div className="mx_RoomHeader_textButton mx_RoomHeader_voipButton"
onClick={this.onMuteVideoClick}>
{
(activeCall.isLocalVideoMuted() ?
"Unmute" : "Mute") + " video"
}
</div>
);
}
{muteVideoButton}
<div className="mx_RoomHeader_textButton mx_RoomHeader_voipButton"
onClick={this.onMuteAudioClick}>
{
(activeCall && activeCall.isMicrophoneMuted() ?
"Unmute" : "Mute") + " audio"
}
</div>
*/
call_buttons = (
<div className="mx_RoomHeader_textButton"
onClick={this.onHangupClick}>
End call
</div>
);
}
var name = null; var name = null;
var searchStatus = null; var searchStatus = null;
var topic_el = null; var topic_el = null;
@ -230,32 +129,9 @@ module.exports = React.createClass({
); );
} }
var zoom_button, video_button, voice_button; var leave_button;
if (activeCall) {
if (activeCall.type == "video") {
zoom_button = (
<div className="mx_RoomHeader_button" onClick={this.onFullscreenClick}>
<img src="img/zoom.png" title="Fullscreen" alt="Fullscreen" width="32" height="32" style={{ 'marginTop': '-5px' }}/>
</div>
);
}
video_button =
<div className="mx_RoomHeader_button mx_RoomHeader_video" onClick={activeCall && activeCall.type === "video" ? this.onMuteVideoClick : this.onVideoClick}>
<img src="img/video.png" title="Video call" alt="Video call" width="32" height="32" style={{ 'marginTop': '-8px' }}/>
</div>;
var img = "img/voip.png";
if (activeCall.isMicrophoneMuted()) {
img = "img/voip-mute.png";
}
voice_button =
<div className="mx_RoomHeader_button mx_RoomHeader_voice" onClick={activeCall ? this.onMuteAudioClick : this.onVoiceClick}>
<img src={img} title="VoIP call" alt="VoIP call" width="32" height="32" style={{ 'marginTop': '-8px' }}/>
</div>;
}
var exit_button;
if (this.props.onLeaveClick) { if (this.props.onLeaveClick) {
exit_button = leave_button =
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton"> <div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
<img src="img/leave.svg" title="Leave room" alt="Leave room" width="26" height="20" onClick={this.props.onLeaveClick}/> <img src="img/leave.svg" title="Leave room" alt="Leave room" width="26" height="20" onClick={this.props.onLeaveClick}/>
</div>; </div>;
@ -272,14 +148,10 @@ module.exports = React.createClass({
{ topic_el } { topic_el }
</div> </div>
</div> </div>
{call_buttons}
{cancel_button} {cancel_button}
{save_button} {save_button}
<div className="mx_RoomHeader_rightRow"> <div className="mx_RoomHeader_rightRow">
{ video_button } { leave_button }
{ voice_button }
{ zoom_button }
{ exit_button }
<div className="mx_RoomHeader_button"> <div className="mx_RoomHeader_button">
<img src="img/search.svg" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/> <img src="img/search.svg" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>
</div> </div>