From 77401e215edb496e426bbf5f35986ae6c85682b2 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 11 Sep 2015 15:49:47 +0100 Subject: [PATCH 01/17] First working outbound conference calling This has a number of failings currently: 1) It needs to hide the 1:1 conference room, 2) Swapping tabs on the outbound call mutes audio (this just seems to be a vector bug since I can repro this on a normal 1:1 voip call), 3) Needs a big plinth/etc to say the conf call is in progress. --- src/CallHandler.js | 59 ++++++++++++++++++++------------- src/ConferenceHandler.js | 70 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 src/ConferenceHandler.js diff --git a/src/CallHandler.js b/src/CallHandler.js index 0915a65a..dbdb84e4 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -57,6 +57,7 @@ var MatrixClientPeg = require("./MatrixClientPeg"); var Modal = require("./Modal"); var ComponentBroker = require('./ComponentBroker'); var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog"); +var ConferenceHandler = require("./ConferenceHandler"); var Matrix = require("matrix-js-sdk"); var dis = require("./dispatcher"); @@ -161,37 +162,49 @@ dis.register(function(payload) { console.error("Room %s does not exist.", payload.room_id); return; } + + function placeCall(newCall) { + _setCallListeners(newCall); + _setCallState(newCall, newCall.roomId, "ringback"); + if (payload.type === 'voice') { + newCall.placeVoiceCall(); + } + else if (payload.type === 'video') { + newCall.placeVideoCall( + payload.remote_element, + payload.local_element + ); + } + else { + console.error("Unknown conf call type: %s", payload.type); + } + } + var members = room.getJoinedMembers(); - if (members.length !== 2) { - var text = members.length === 1 ? "yourself." : "more than 2 people."; + if (members.length <= 1) { Modal.createDialog(ErrorDialog, { - description: "You cannot place a call with " + text + description: "You cannot place a call with yourself." }); - console.error( - "Fail: There are %s joined members in this room, not 2.", - room.getJoinedMembers().length - ); return; } - console.log("Place %s call in %s", payload.type, payload.room_id); - var call = Matrix.createNewMatrixCall( - MatrixClientPeg.get(), payload.room_id - ); - _setCallListeners(call); - _setCallState(call, call.roomId, "ringback"); - if (payload.type === 'voice') { - call.placeVoiceCall(); - } - else if (payload.type === 'video') { - call.placeVideoCall( - payload.remote_element, - payload.local_element + else if (members.length === 2) { + console.log("Place %s call in %s", payload.type, payload.room_id); + var call = Matrix.createNewMatrixCall( + MatrixClientPeg.get(), payload.room_id ); + placeCall(call); } - else { - console.error("Unknown call type: %s", payload.type); + else { // > 2 + console.log("Place conference call in %s", payload.room_id); + var confHandler = new ConferenceHandler( + MatrixClientPeg.get(), payload.room_id + ); + confHandler.setup().done(function(call) { + placeCall(call); + }, function(err) { + console.error("Failed to setup conference call: %s", err); + }); } - break; case 'incoming_call': if (calls[payload.call.roomId]) { diff --git a/src/ConferenceHandler.js b/src/ConferenceHandler.js new file mode 100644 index 00000000..e4d0f1f7 --- /dev/null +++ b/src/ConferenceHandler.js @@ -0,0 +1,70 @@ +"use strict"; +var q = require("q"); +var Matrix = require("matrix-js-sdk"); +var Room = Matrix.Room; + +var USER_PREFIX = "fs_"; +var DOMAIN = "matrix.org"; + +function ConferenceHandler(matrixClient, groupChatRoomId) { + this.client = matrixClient; + this.groupRoomId = groupChatRoomId; + // abuse browserify's core node Buffer support (strip padding ='s) + this.base64RoomId = new Buffer(this.groupRoomId).toString("base64").replace(/=/g, ""); + this.confUserId = "@" + USER_PREFIX + this.base64RoomId + ":" + DOMAIN; +} + +ConferenceHandler.prototype.setup = function() { + var self = this; + return this._joinConferenceUser().then(function() { + return self._getConferenceUserRoom(); + }).then(function(room) { + // return a call for *this* room to be placed. + return Matrix.createNewMatrixCall(self.client, room.roomId); + }); +}; + +ConferenceHandler.prototype._joinConferenceUser = function() { + // Make sure the conference user is in the group chat room + var groupRoom = this.client.getRoom(this.groupRoomId); + if (!groupRoom) { + return q.reject("Bad group room ID"); + } + var members = groupRoom.getJoinedMembers(); + var confUserExists = false; + for (var i = 0; i < members.length; i++) { + if (members[i].userId === this.confUserId) { + confUserExists = true; + break; + } + } + if (confUserExists) { + return q(); + } + return this.client.invite(this.groupRoomId, this.confUserId); +}; + +ConferenceHandler.prototype._getConferenceUserRoom = function() { + // Use an existing 1:1 with the conference user; else make one + var rooms = this.client.getRooms(); + var confRoom = null; + for (var i = 0; i < rooms.length; i++) { + if (rooms[i].hasMembershipState(this.confUserId, "join") && + rooms[i].getJoinedMembers().length === 2) { + confRoom = rooms[i]; + break; + } + } + if (confRoom) { + return q(confRoom); + } + return this.client.createRoom({ + preset: "private_chat", + invite: [this.confUserId] + }).then(function(res) { + return new Room(res.room_id); + }); +}; + +module.exports = ConferenceHandler; + From e3b02a295c987f52f89232b24de4236acb1e0c1b Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 11 Sep 2015 16:14:30 +0100 Subject: [PATCH 02/17] Check conf user/rooms a bit more efficiently --- src/ConferenceHandler.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/ConferenceHandler.js b/src/ConferenceHandler.js index e4d0f1f7..ef360943 100644 --- a/src/ConferenceHandler.js +++ b/src/ConferenceHandler.js @@ -30,15 +30,8 @@ ConferenceHandler.prototype._joinConferenceUser = function() { if (!groupRoom) { return q.reject("Bad group room ID"); } - var members = groupRoom.getJoinedMembers(); - var confUserExists = false; - for (var i = 0; i < members.length; i++) { - if (members[i].userId === this.confUserId) { - confUserExists = true; - break; - } - } - if (confUserExists) { + var member = groupRoom.getMember(this.confUserId); + if (member && member.membership === "join") { return q(); } return this.client.invite(this.groupRoomId, this.confUserId); @@ -49,7 +42,8 @@ ConferenceHandler.prototype._getConferenceUserRoom = function() { var rooms = this.client.getRooms(); var confRoom = null; for (var i = 0; i < rooms.length; i++) { - if (rooms[i].hasMembershipState(this.confUserId, "join") && + var confUser = rooms[i].getMember(this.confUserId); + if (confUser && confUser.membership === "join" && rooms[i].getJoinedMembers().length === 2) { confRoom = rooms[i]; break; From fc892b3580ea6b6f8ed10f82b2aa952760db38a7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 11 Sep 2015 16:55:48 +0100 Subject: [PATCH 03/17] Hide 1:1 conference rooms --- src/CallHandler.js | 6 ++--- src/ConferenceHandler.js | 32 +++++++++++++++++++++------ src/controllers/organisms/RoomList.js | 22 +++++++++++++++++- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index dbdb84e4..42cc5d57 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -57,7 +57,7 @@ var MatrixClientPeg = require("./MatrixClientPeg"); var Modal = require("./Modal"); var ComponentBroker = require('./ComponentBroker'); var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog"); -var ConferenceHandler = require("./ConferenceHandler"); +var ConferenceCall = require("./ConferenceHandler").ConferenceCall; var Matrix = require("matrix-js-sdk"); var dis = require("./dispatcher"); @@ -196,10 +196,10 @@ dis.register(function(payload) { } else { // > 2 console.log("Place conference call in %s", payload.room_id); - var confHandler = new ConferenceHandler( + var confCall = new ConferenceCall( MatrixClientPeg.get(), payload.room_id ); - confHandler.setup().done(function(call) { + confCall.setup().done(function(call) { placeCall(call); }, function(err) { console.error("Failed to setup conference call: %s", err); diff --git a/src/ConferenceHandler.js b/src/ConferenceHandler.js index ef360943..6a43fe24 100644 --- a/src/ConferenceHandler.js +++ b/src/ConferenceHandler.js @@ -6,15 +6,15 @@ var Room = Matrix.Room; var USER_PREFIX = "fs_"; var DOMAIN = "matrix.org"; -function ConferenceHandler(matrixClient, groupChatRoomId) { +function ConferenceCall(matrixClient, groupChatRoomId) { this.client = matrixClient; this.groupRoomId = groupChatRoomId; // abuse browserify's core node Buffer support (strip padding ='s) - this.base64RoomId = new Buffer(this.groupRoomId).toString("base64").replace(/=/g, ""); - this.confUserId = "@" + USER_PREFIX + this.base64RoomId + ":" + DOMAIN; + var base64RoomId = new Buffer(groupChatRoomId).toString("base64").replace(/=/g, ""); + this.confUserId = "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN; } -ConferenceHandler.prototype.setup = function() { +ConferenceCall.prototype.setup = function() { var self = this; return this._joinConferenceUser().then(function() { return self._getConferenceUserRoom(); @@ -24,7 +24,7 @@ ConferenceHandler.prototype.setup = function() { }); }; -ConferenceHandler.prototype._joinConferenceUser = function() { +ConferenceCall.prototype._joinConferenceUser = function() { // Make sure the conference user is in the group chat room var groupRoom = this.client.getRoom(this.groupRoomId); if (!groupRoom) { @@ -37,7 +37,7 @@ ConferenceHandler.prototype._joinConferenceUser = function() { return this.client.invite(this.groupRoomId, this.confUserId); }; -ConferenceHandler.prototype._getConferenceUserRoom = function() { +ConferenceCall.prototype._getConferenceUserRoom = function() { // Use an existing 1:1 with the conference user; else make one var rooms = this.client.getRooms(); var confRoom = null; @@ -60,5 +60,23 @@ ConferenceHandler.prototype._getConferenceUserRoom = function() { }); }; -module.exports = ConferenceHandler; +/** + * Check if this room member is in fact a conference bot. + * @param {RoomMember} The room member to check + * @return {boolean} True if it is a conference bot. + */ +module.exports.isConferenceUser = function(roomMember) { + if (roomMember.userId.indexOf("@" + USER_PREFIX) !== 0) { + return false; + } + var base64part = roomMember.userId.split(":")[0].substring(1 + USER_PREFIX.length); + if (base64part) { + var decoded = new Buffer(base64part, "base64").toString(); + // ! $STUFF : $STUFF + return /^!.+:.+/.test(decoded); + } + return false; +}; + +module.exports.ConferenceCall = ConferenceCall; diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index 91c384a0..bc58ed79 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -21,9 +21,12 @@ var MatrixClientPeg = require("../../MatrixClientPeg"); var RoomListSorter = require("../../RoomListSorter"); var ComponentBroker = require('../../ComponentBroker'); +var ConferenceHandler = require("../../ConferenceHandler"); var RoomTile = ComponentBroker.get("molecules/RoomTile"); +var HIDE_CONFERENCE_CHANS = true; + module.exports = { componentWillMount: function() { var cli = MatrixClientPeg.get(); @@ -97,7 +100,24 @@ module.exports = { return RoomListSorter.mostRecentActivityFirst( MatrixClientPeg.get().getRooms().filter(function(room) { var member = room.getMember(MatrixClientPeg.get().credentials.userId); - return member && (member.membership == "join" || member.membership == "invite"); + var shouldShowRoom = ( + member && (member.membership == "join" || member.membership == "invite") + ); + // hiding conf rooms only ever toggles shouldShowRoom to false + if (shouldShowRoom && HIDE_CONFERENCE_CHANS) { + // we want to hide the 1:1 conf<->user room and not the group chat + var joinedMembers = room.getJoinedMembers(); + if (joinedMembers.length === 2) { + var otherMember = joinedMembers.filter(function(m) { + return m.userId !== member.userId + })[0]; + if (ConferenceHandler.isConferenceUser(otherMember)) { + console.log("Hiding conference 1:1 room %s", room.roomId); + shouldShowRoom = false; + } + } + } + return shouldShowRoom; }) ); }, From 59986d8b7232d17179531d8b1ac762471f8416a4 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Sep 2015 11:05:53 +0100 Subject: [PATCH 04/17] Pass the call around different CallViews to keep media flowing Previously, the CallView was attached to the RoomView, so you would get a new CallView each time you changed the room and the one you changed from would be destroyed. This would destroy media capture/playback as the element was no longer in the DOM. This is now fixed by having a "global" CallView which is attached at the MatrixChat "page" level in the DOM hierarchy. This CallView isn't scoped to a particular room; it will render any "active" call it can find that *isn't the current room being displayed*. This has the side effect of enforcing 1 call per app semantics as only the first active call found is returned. This fixes https://github.com/vector-im/vector-web/issues/31 This is unfinished (CSS for the global call view isn't done) --- skins/base/views/pages/MatrixChat.js | 19 ++++++++++++++++-- src/CallHandler.js | 13 +++++++++++- src/controllers/molecules/voip/CallView.js | 23 +++++++++++++++++++--- src/controllers/pages/MatrixChat.js | 8 ++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js index 1b756792..6b1ddb63 100644 --- a/skins/base/views/pages/MatrixChat.js +++ b/skins/base/views/pages/MatrixChat.js @@ -18,6 +18,7 @@ limitations under the License. var React = require('react'); var ComponentBroker = require('../../../../src/ComponentBroker'); +var CallHandler = require('../../../../src/CallHandler'); var LeftPanel = ComponentBroker.get('organisms/LeftPanel'); var RoomView = ComponentBroker.get('organisms/RoomView'); @@ -29,8 +30,9 @@ var CreateRoom = ComponentBroker.get('organisms/CreateRoom'); var RoomDirectory = ComponentBroker.get('organisms/RoomDirectory'); var MatrixToolbar = ComponentBroker.get('molecules/MatrixToolbar'); var Notifier = ComponentBroker.get('organisms/Notifier'); +var CallView = ComponentBroker.get('molecules/voip/CallView'); -var MatrixChatController = require("../../../../src/controllers/pages/MatrixChat"); +var MatrixChatController = require('../../../../src/controllers/pages/MatrixChat'); // should be atomised var Loader = require("react-loader"); @@ -53,7 +55,7 @@ module.exports = React.createClass({ render: function() { if (this.state.logged_in && this.state.ready) { - var page_element; + var page_element, call_element; var right_panel = ""; switch (this.state.page_type) { @@ -74,6 +76,17 @@ module.exports = React.createClass({ right_panel = break; } + // if we aren't viewing a room with an ongoing call, but there is an + // active call, show the call element - we need to do this to make + // audio/video not crap out + if (this.state.active_call && ( + !this.state.currentRoom || !CallHandler.getCall(this.state.currentRoom))) { + console.log( + "Creating global CallView for active call in room %s", + this.state.active_call.roomId + ); + call_element = + } if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { return ( @@ -81,6 +94,7 @@ module.exports = React.createClass({
+ {call_element}
{page_element}
@@ -93,6 +107,7 @@ module.exports = React.createClass({ return (
+ {call_element}
{page_element}
diff --git a/src/CallHandler.js b/src/CallHandler.js index 42cc5d57..28fd77e7 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -106,7 +106,7 @@ function _setCallListeners(call) { play("ringbackAudio"); } else if (newState === "ended" && oldState === "connected") { - _setCallState(call, call.roomId, "ended"); + _setCallState(undefined, call.roomId, "ended"); pause("ringbackAudio"); play("callendAudio"); } @@ -239,5 +239,16 @@ dis.register(function(payload) { module.exports = { getCall: function(roomId) { return calls[roomId] || null; + }, + + getAnyActiveCall: function() { + var roomsWithCalls = Object.keys(calls); + for (var i = 0; i < roomsWithCalls.length; i++) { + if (calls[roomsWithCalls[i]] && + calls[roomsWithCalls[i]].call_state !== "ended") { + return calls[roomsWithCalls[i]]; + } + } + return null; } }; \ No newline at end of file diff --git a/src/controllers/molecules/voip/CallView.js b/src/controllers/molecules/voip/CallView.js index e43046a5..6e6f3482 100644 --- a/src/controllers/molecules/voip/CallView.js +++ b/src/controllers/molecules/voip/CallView.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; var dis = require("../../../dispatcher"); var CallHandler = require("../../../CallHandler"); +var MatrixClientPeg = require("../../../MatrixClientPeg"); /* * State vars: @@ -24,14 +25,30 @@ var CallHandler = require("../../../CallHandler"); * * Props: * this.props.room = Room (JS SDK) + * + * Internal state: + * this._trackedRoom = (either from props.room or programatically set) */ module.exports = { componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); + this._trackedRoom = null; if (this.props.room) { - this.showCall(this.props.room.roomId); + this._trackedRoom = this.props.room; + this.showCall(this._trackedRoom.roomId); + } + else { + var call = CallHandler.getAnyActiveCall(); + if (call) { + console.log( + "Global CallView is now tracking active call in room %s", + call.roomId + ); + this._trackedRoom = MatrixClientPeg.get().getRoom(call.roomId); + this.showCall(call.roomId); + } } }, @@ -41,8 +58,8 @@ module.exports = { onAction: function(payload) { // if we were given a room_id to track, don't handle anything else. - if (payload.room_id && this.props.room && - this.props.room.roomId !== payload.room_id) { + if (payload.room_id && this._trackedRoom && + this._trackedRoom.roomId !== payload.room_id) { return; } if (payload.action !== 'call_state') { diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 08cc652d..b98f42c0 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -26,6 +26,7 @@ var dis = require("../../dispatcher"); var q = require("q"); var ComponentBroker = require('../../ComponentBroker'); +var CallHandler = require("../../CallHandler"); var Notifier = ComponentBroker.get('organisms/Notifier'); var MatrixTools = require('../../MatrixTools'); @@ -205,6 +206,13 @@ module.exports = { case 'notifier_enabled': this.forceUpdate(); break; + case 'call_state': + // listen for call state changes to prod the render method, which + // may hide the global CallView if the call it is tracking is dead + this.setState({ + active_call: CallHandler.getAnyActiveCall() + }); + break; } }, From 5e3698de640ecb91b1c969bd3031e911dbdac43a Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Sep 2015 11:43:51 +0100 Subject: [PATCH 05/17] Actually enforce 1 call semantics. --- src/CallHandler.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 28fd77e7..2cfd114f 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -154,7 +154,11 @@ function _setCallState(call, roomId, status) { dis.register(function(payload) { switch (payload.action) { case 'place_call': - if (calls[payload.room_id]) { + if (module.exports.getAnyActiveCall()) { + Modal.createDialog(ErrorDialog, { + title: "Existing Call", + description: "You are already in a call." + }); return; // don't allow >1 call to be placed. } var room = MatrixClientPeg.get().getRoom(payload.room_id); @@ -207,7 +211,7 @@ dis.register(function(payload) { } break; case 'incoming_call': - if (calls[payload.call.roomId]) { + if (module.exports.getAnyActiveCall()) { payload.call.hangup("busy"); return; // don't allow >1 call to be received, hangup newer one. } From 7866979c79fd03ad54f23067914520ea067ce875 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Sep 2015 13:04:09 +0100 Subject: [PATCH 06/17] Show/hide the Hangup button depending on the state of the conf call. --- src/ConferenceHandler.js | 18 +++++++---- src/controllers/molecules/RoomHeader.js | 40 +++++++++++++++++++------ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/ConferenceHandler.js b/src/ConferenceHandler.js index 6a43fe24..4f136806 100644 --- a/src/ConferenceHandler.js +++ b/src/ConferenceHandler.js @@ -9,9 +9,7 @@ var DOMAIN = "matrix.org"; function ConferenceCall(matrixClient, groupChatRoomId) { this.client = matrixClient; this.groupRoomId = groupChatRoomId; - // abuse browserify's core node Buffer support (strip padding ='s) - var base64RoomId = new Buffer(groupChatRoomId).toString("base64").replace(/=/g, ""); - this.confUserId = "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN; + this.confUserId = module.exports.getConferenceUserIdForRoom(this.groupRoomId); } ConferenceCall.prototype.setup = function() { @@ -19,8 +17,12 @@ ConferenceCall.prototype.setup = function() { return this._joinConferenceUser().then(function() { return self._getConferenceUserRoom(); }).then(function(room) { - // return a call for *this* room to be placed. - return Matrix.createNewMatrixCall(self.client, room.roomId); + // return a call for *this* room to be placed. We also tack on + // confUserId to speed up lookups (else we'd need to loop every room + // looking for a 1:1 room with this conf user ID!) + var call = Matrix.createNewMatrixCall(self.client, room.roomId); + call.confUserId = self.confUserId; + return call; }); }; @@ -78,5 +80,11 @@ module.exports.isConferenceUser = function(roomMember) { return false; }; +module.exports.getConferenceUserIdForRoom = function(roomId) { + // abuse browserify's core node Buffer support (strip padding ='s) + var base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, ""); + return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN; +}; + module.exports.ConferenceCall = ConferenceCall; diff --git a/src/controllers/molecules/RoomHeader.js b/src/controllers/molecules/RoomHeader.js index 2ef99953..21ffd632 100644 --- a/src/controllers/molecules/RoomHeader.js +++ b/src/controllers/molecules/RoomHeader.js @@ -19,11 +19,15 @@ limitations under the License. /* * State vars: * this.state.call_state = the UI state of the call (see CallHandler) + * + * Props: + * room (JS SDK Room) */ var React = require('react'); var dis = require("../../dispatcher"); var CallHandler = require("../../CallHandler"); +var ConferenceHandler = require("../../ConferenceHandler"); module.exports = { propTypes: { @@ -44,7 +48,7 @@ module.exports = { componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); if (this.props.room) { - var call = CallHandler.getCall(this.props.room.roomId); + var call = this._getCall(this.props.room.roomId); var callState = call ? call.call_state : "ended"; this.setState({ call_state: callState @@ -57,15 +61,12 @@ module.exports = { }, onAction: function(payload) { - // if we were given a room_id to track, don't handle anything else. - if (payload.room_id && this.props.room && - this.props.room.roomId !== payload.room_id) { + // 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; } - if (payload.action !== 'call_state') { - return; - } - var call = CallHandler.getCall(payload.room_id); + var call = this._getCall(payload.room_id); var callState = call ? call.call_state : "ended"; this.setState({ call_state: callState @@ -87,9 +88,30 @@ module.exports = { }); }, onHangupClick: function() { + var call = this._getCall(this.props.room.roomId); + if (!call) { return; } dis.dispatch({ action: 'hangup', - room_id: this.props.room.roomId + // 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 }); + }, + + _getCall: function(roomId) { + var call = CallHandler.getCall(roomId); + if (!call) { + // search for a conference 1:1 call + var activeCall = CallHandler.getAnyActiveCall(); + if (activeCall && activeCall.confUserId) { + var thisRoomConfUserId = ConferenceHandler.getConferenceUserIdForRoom( + roomId + ); + if (thisRoomConfUserId === activeCall.confUserId) { + call = activeCall; + } + } + } + return call; } }; From 353269370fc58d9662ca5290cb7b201c70a998bf Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Sep 2015 13:19:07 +0100 Subject: [PATCH 07/17] Wire up the "room" CallView for conferencing This also separates out concerns better - UI elements just need to poke getCallForRoom rather than care if the thing they are displaying is a true 1:1 for this room ID or actually a conf room. --- skins/base/views/pages/MatrixChat.js | 3 ++- src/CallHandler.js | 23 +++++++++++++++++++++ src/controllers/molecules/RoomHeader.js | 24 +++------------------- src/controllers/molecules/voip/CallView.js | 11 ++++------ 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js index 6b1ddb63..e5fbe178 100644 --- a/skins/base/views/pages/MatrixChat.js +++ b/skins/base/views/pages/MatrixChat.js @@ -80,7 +80,8 @@ module.exports = React.createClass({ // active call, show the call element - we need to do this to make // audio/video not crap out if (this.state.active_call && ( - !this.state.currentRoom || !CallHandler.getCall(this.state.currentRoom))) { + !this.state.currentRoom || + !CallHandler.getCallForRoom(this.state.currentRoom))) { console.log( "Creating global CallView for active call in room %s", this.state.active_call.roomId diff --git a/src/CallHandler.js b/src/CallHandler.js index 2cfd114f..025ece38 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -58,6 +58,7 @@ var Modal = require("./Modal"); var ComponentBroker = require('./ComponentBroker'); var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog"); var ConferenceCall = require("./ConferenceHandler").ConferenceCall; +var ConferenceHandler = require("./ConferenceHandler"); var Matrix = require("matrix-js-sdk"); var dis = require("./dispatcher"); @@ -241,10 +242,32 @@ dis.register(function(payload) { }); module.exports = { + + getCallForRoom: function(roomId) { + return ( + module.exports.getCall(roomId) || + module.exports.getConferenceCall(roomId) + ); + }, + getCall: function(roomId) { return calls[roomId] || null; }, + getConferenceCall: function(roomId) { + // search for a conference 1:1 call for this group chat room ID + var activeCall = module.exports.getAnyActiveCall(); + if (activeCall && activeCall.confUserId) { + var thisRoomConfUserId = ConferenceHandler.getConferenceUserIdForRoom( + roomId + ); + if (thisRoomConfUserId === activeCall.confUserId) { + return activeCall; + } + } + return null; + }, + getAnyActiveCall: function() { var roomsWithCalls = Object.keys(calls); for (var i = 0; i < roomsWithCalls.length; i++) { diff --git a/src/controllers/molecules/RoomHeader.js b/src/controllers/molecules/RoomHeader.js index 21ffd632..c7e023fc 100644 --- a/src/controllers/molecules/RoomHeader.js +++ b/src/controllers/molecules/RoomHeader.js @@ -27,7 +27,6 @@ limitations under the License. var React = require('react'); var dis = require("../../dispatcher"); var CallHandler = require("../../CallHandler"); -var ConferenceHandler = require("../../ConferenceHandler"); module.exports = { propTypes: { @@ -48,7 +47,7 @@ module.exports = { componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); if (this.props.room) { - var call = this._getCall(this.props.room.roomId); + var call = CallHandler.getCallForRoom(this.props.room.roomId); var callState = call ? call.call_state : "ended"; this.setState({ call_state: callState @@ -66,7 +65,7 @@ module.exports = { if (payload.action !== 'call_state' || !payload.room_id) { return; } - var call = this._getCall(payload.room_id); + var call = CallHandler.getCallForRoom(payload.room_id); var callState = call ? call.call_state : "ended"; this.setState({ call_state: callState @@ -88,7 +87,7 @@ module.exports = { }); }, onHangupClick: function() { - var call = this._getCall(this.props.room.roomId); + var call = CallHandler.getCallForRoom(this.props.room.roomId); if (!call) { return; } dis.dispatch({ action: 'hangup', @@ -96,22 +95,5 @@ module.exports = { // (e.g. conferences which will hangup the 1:1 room instead) room_id: call.roomId }); - }, - - _getCall: function(roomId) { - var call = CallHandler.getCall(roomId); - if (!call) { - // search for a conference 1:1 call - var activeCall = CallHandler.getAnyActiveCall(); - if (activeCall && activeCall.confUserId) { - var thisRoomConfUserId = ConferenceHandler.getConferenceUserIdForRoom( - roomId - ); - if (thisRoomConfUserId === activeCall.confUserId) { - call = activeCall; - } - } - } - return call; } }; diff --git a/src/controllers/molecules/voip/CallView.js b/src/controllers/molecules/voip/CallView.js index 6e6f3482..3c735be1 100644 --- a/src/controllers/molecules/voip/CallView.js +++ b/src/controllers/molecules/voip/CallView.js @@ -57,19 +57,16 @@ module.exports = { }, onAction: function(payload) { - // if we were given a room_id to track, don't handle anything else. - if (payload.room_id && this._trackedRoom && - this._trackedRoom.roomId !== payload.room_id) { - return; - } - if (payload.action !== 'call_state') { + // 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; } this.showCall(payload.room_id); }, showCall: function(roomId) { - var call = CallHandler.getCall(roomId); + var call = CallHandler.getCallForRoom(roomId); if (call) { call.setLocalVideoElement(this.getVideoView().getLocalVideoElement()); // N.B. the remote video element is used for playback for audio for voice calls From f384aa7d9ec14645736857ab2e5ffd5e0529cead Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Sep 2015 14:18:17 +0100 Subject: [PATCH 08/17] Add notification to group chat rooms with ongoing conf calls This notification disappears when in the conf call / when the call is over. CSS stolen from the desktop notification bar. --- skins/base/css/organisms/RoomView.css | 9 +++++ skins/base/views/organisms/RoomView.js | 10 +++++ skins/base/views/pages/MatrixChat.js | 1 + src/controllers/organisms/RoomList.js | 2 +- src/controllers/organisms/RoomView.js | 56 +++++++++++++++++++++----- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/skins/base/css/organisms/RoomView.css b/skins/base/css/organisms/RoomView.css index fb58b204..e1c589a2 100644 --- a/skins/base/css/organisms/RoomView.css +++ b/skins/base/css/organisms/RoomView.css @@ -218,3 +218,12 @@ limitations under the License. background-color: blue; height: 5px; } + +.mx_RoomView_ongoingConfCallNotification { + width: 100%; + text-align: center; + background-color: #ff0064; + color: #fff; + font-weight: bold; + padding: 6px; +} \ No newline at end of file diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js index 7a22b017..0f1fe1af 100644 --- a/skins/base/views/organisms/RoomView.js +++ b/skins/base/views/organisms/RoomView.js @@ -176,6 +176,15 @@ module.exports = React.createClass({ roomEdit = ; } + var conferenceCallNotification = null; + if (this.state.displayConfCallNotification) { + conferenceCallNotification = ( +
+ Ongoing conference call +
+ ); + } + var fileDropTarget = null; if (this.state.draggingFile) { fileDropTarget =
@@ -192,6 +201,7 @@ module.exports = React.createClass({ onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} />
+ { conferenceCallNotification } { roomEdit }
diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js index e5fbe178..602e241e 100644 --- a/skins/base/views/pages/MatrixChat.js +++ b/skins/base/views/pages/MatrixChat.js @@ -89,6 +89,7 @@ module.exports = React.createClass({ call_element = } + // TODO: Fix duplication here and do conditionals like we do above if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { return (
diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index bc58ed79..6acce59a 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -112,7 +112,7 @@ module.exports = { return m.userId !== member.userId })[0]; if (ConferenceHandler.isConferenceUser(otherMember)) { - console.log("Hiding conference 1:1 room %s", room.roomId); + // console.log("Hiding conference 1:1 room %s", room.roomId); shouldShowRoom = false; } } diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index d3feff69..459a3606 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -31,7 +31,8 @@ var dis = require("../../dispatcher"); var PAGINATE_SIZE = 20; var INITIAL_SIZE = 100; -var ComponentBroker = require('../../ComponentBroker'); +var ConferenceHandler = require("../../ConferenceHandler"); +var CallHandler = require("../../CallHandler"); var Notifier = ComponentBroker.get('organisms/Notifier'); var tileTypes = { @@ -62,6 +63,7 @@ module.exports = { MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); + MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); this.atBottom = true; }, @@ -78,6 +80,7 @@ module.exports = { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); + MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); } }, @@ -94,15 +97,20 @@ module.exports = { this.forceUpdate(); break; case 'call_state': - if (this.props.roomId !== payload.room_id) { - break; - } - // scroll to bottom - var messageWrapper = this.refs.messageWrapper; - if (messageWrapper) { - messageWrapper = messageWrapper.getDOMNode(); - messageWrapper.scrollTop = messageWrapper.scrollHeight; + if (CallHandler.getCallForRoom(this.props.roomId)) { + // Call state has changed so we may be loading video elements + // which will obscure the message log. + // scroll to bottom + var messageWrapper = this.refs.messageWrapper; + if (messageWrapper) { + messageWrapper = messageWrapper.getDOMNode(); + messageWrapper.scrollTop = messageWrapper.scrollHeight; + } } + + // possibly remove the conf call notification if we're now in + // the conf + this._updateConfCallNotification(); break; } }, @@ -170,6 +178,35 @@ module.exports = { this.forceUpdate(); }, + onRoomStateMember: function(ev, state, member) { + if (member.roomId !== this.props.roomId || + member.userId !== ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { + return; + } + this._updateConfCallNotification(); + }, + + _updateConfCallNotification: function() { + var member = MatrixClientPeg.get().getRoom(this.props.roomId).getMember( + ConferenceHandler.getConferenceUserIdForRoom(this.props.roomId) + ); + + if (!member) { + return; + } + console.log("_updateConfCallNotification"); + var confCall = CallHandler.getConferenceCall(member.roomId); + + // A conf call notification should be displayed if there is an ongoing + // conf call but this cilent isn't a part of it. + this.setState({ + displayConfCallNotification: ( + (!confCall || confCall.call_state === "ended") && + member.membership === "join" + ) + }); + }, + componentDidMount: function() { if (this.refs.messageWrapper) { var messageWrapper = this.refs.messageWrapper.getDOMNode(); @@ -183,6 +220,7 @@ module.exports = { this.fillSpace(); } + this._updateConfCallNotification(); }, componentDidUpdate: function() { From 370310bf82620049ac5620c81163f5407a116aed Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Sep 2015 15:02:02 +0100 Subject: [PATCH 09/17] Use better variable names --- src/controllers/organisms/RoomList.js | 6 +++--- src/controllers/organisms/RoomView.js | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index 6acce59a..da571552 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -99,9 +99,9 @@ module.exports = { getRoomList: function() { return RoomListSorter.mostRecentActivityFirst( MatrixClientPeg.get().getRooms().filter(function(room) { - var member = room.getMember(MatrixClientPeg.get().credentials.userId); + var me = room.getMember(MatrixClientPeg.get().credentials.userId); var shouldShowRoom = ( - member && (member.membership == "join" || member.membership == "invite") + me && (me.membership == "join" || me.membership == "invite") ); // hiding conf rooms only ever toggles shouldShowRoom to false if (shouldShowRoom && HIDE_CONFERENCE_CHANS) { @@ -109,7 +109,7 @@ module.exports = { var joinedMembers = room.getJoinedMembers(); if (joinedMembers.length === 2) { var otherMember = joinedMembers.filter(function(m) { - return m.userId !== member.userId + return m.userId !== me.userId })[0]; if (ConferenceHandler.isConferenceUser(otherMember)) { // console.log("Hiding conference 1:1 room %s", room.roomId); diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 459a3606..1856cac9 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -187,22 +187,21 @@ module.exports = { }, _updateConfCallNotification: function() { - var member = MatrixClientPeg.get().getRoom(this.props.roomId).getMember( + var confMember = MatrixClientPeg.get().getRoom(this.props.roomId).getMember( ConferenceHandler.getConferenceUserIdForRoom(this.props.roomId) ); - if (!member) { + if (!confMember) { return; } - console.log("_updateConfCallNotification"); - var confCall = CallHandler.getConferenceCall(member.roomId); + var confCall = CallHandler.getConferenceCall(confMember.roomId); // A conf call notification should be displayed if there is an ongoing // conf call but this cilent isn't a part of it. this.setState({ displayConfCallNotification: ( (!confCall || confCall.call_state === "ended") && - member.membership === "join" + confMember.membership === "join" ) }); }, From 2b65b4c2dc60172cb05c60fbdb2f7855ad4a0a70 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Sep 2015 15:49:33 +0100 Subject: [PATCH 10/17] Hide the local video when in a conf call --- src/controllers/molecules/voip/CallView.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/molecules/voip/CallView.js b/src/controllers/molecules/voip/CallView.js index 3c735be1..a20e4463 100644 --- a/src/controllers/molecules/voip/CallView.js +++ b/src/controllers/molecules/voip/CallView.js @@ -73,7 +73,11 @@ module.exports = { call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement()); } if (call && call.type === "video" && call.state !== 'ended') { - this.getVideoView().getLocalVideoElement().style.display = "initial"; + // if this call is a conf call, don't display local video as the + // conference will have us in it + this.getVideoView().getLocalVideoElement().style.display = ( + call.confUserId ? "none" : "initial" + ); this.getVideoView().getRemoteVideoElement().style.display = "initial"; } else { From f89fbffe89a960af373153f2019dc5d78e09fd1e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 15 Sep 2015 15:55:02 +0100 Subject: [PATCH 11/17] Auto-place a video call if the conf notification is clicked --- skins/base/views/organisms/RoomView.js | 2 +- src/controllers/organisms/RoomView.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js index 0f1fe1af..b6747ae7 100644 --- a/skins/base/views/organisms/RoomView.js +++ b/skins/base/views/organisms/RoomView.js @@ -179,7 +179,7 @@ module.exports = React.createClass({ var conferenceCallNotification = null; if (this.state.displayConfCallNotification) { conferenceCallNotification = ( -
+
Ongoing conference call
); diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 1856cac9..c6881de3 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -206,6 +206,14 @@ module.exports = { }); }, + onConferenceNotificationClick: function() { + dis.dispatch({ + action: 'place_call', + type: "video", + room_id: this.props.roomId + }); + }, + componentDidMount: function() { if (this.refs.messageWrapper) { var messageWrapper = this.refs.messageWrapper.getDOMNode(); From 0aec086ebbdf333cf7f5bc47838d80d1118c7c49 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 15 Sep 2015 17:05:49 +0100 Subject: [PATCH 12/17] actually link to blog etc from the login page --- skins/base/css/templates/Login.css | 22 ++++++++++++++++++---- skins/base/views/templates/Login.js | 7 +++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/skins/base/css/templates/Login.css b/skins/base/css/templates/Login.css index 7362ac50..93cb7433 100644 --- a/skins/base/css/templates/Login.css +++ b/skins/base/css/templates/Login.css @@ -75,6 +75,22 @@ limitations under the License. opacity: 0.8; } +.mx_Login_create:link { + color: #4a4a4a; +} + +.mx_Login_links { + display: block; + text-align: center; + width: 100%; + font-size: 14px; + opacity: 0.8; +} + +.mx_Login_links a:link { + color: #4a4a4a; +} + .mx_Login_loader { position: absolute; left: 50%; @@ -85,12 +101,10 @@ limitations under the License. color: #ff2020; font-weight: bold; text-align: center; +/* height: 24px; +*/ margin-top: 12px; margin-bottom: 12px; } -.mx_Login_create:link { - color: #4a4a4a; -} - diff --git a/skins/base/views/templates/Login.js b/skins/base/views/templates/Login.js index 57b5fbcd..4e13aaba 100644 --- a/skins/base/views/templates/Login.js +++ b/skins/base/views/templates/Login.js @@ -165,6 +165,13 @@ module.exports = React.createClass({ {this.state.errorText}
Create a new account +
+
+ blog  ·   + twitter  ·   + github  ·   + powered by Matrix +
); }, From e991beb9004b009c42fba2375f4625c2082ac3eb Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 17 Sep 2015 11:06:08 +0100 Subject: [PATCH 13/17] Add conferencing doc --- docs/conferencing.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/conferencing.md diff --git a/docs/conferencing.md b/docs/conferencing.md new file mode 100644 index 00000000..e69de29b From 9c8b540d1416f239643c55386d4b91c0abae7b14 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 17 Sep 2015 11:06:50 +0100 Subject: [PATCH 14/17] Actually add the doc --- docs/conferencing.md | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/conferencing.md b/docs/conferencing.md index e69de29b..874ce4cf 100644 --- a/docs/conferencing.md +++ b/docs/conferencing.md @@ -0,0 +1,52 @@ +# VoIP Conferencing + +This is a draft proposal for a naive voice/video conferencing implementation for +Matrix clients. There are many possible conferencing architectures possible for +Matrix (Multipoint Conferencing Unit (MCU); Stream Forwarding Unit (SFU); Peer- +to-Peer mesh (P2P), etc; events shared in the group room; events shared 1:1; +possibly even out-of-band signalling). + +This is a starting point for a naive MCU implementation which could provide one +possible Matrix-wide solution in future, which retains backwards compatibility +with standard 1:1 calling. + + * A client chooses to initiate a conference for a given room by starting a + voice or video call with a 'conference focus' user. This is a virtual user + (typically Application Service) which implements a conferencing bridge. It + isn't defined how the client discovers or selects this user. + + * The conference focus user MUST join the room in which the client has + initiated the conference - this may require the client to invite the + conference focus user to the room, depending on the room's `join_rules`. The + conference focus user needs to be in the room to let the bridge eject users + from the conference who have left the room in which it was initiated, and aid + discovery of the conference by other users in the room. The bridge + identifies the room to join based on the user ID by which it was invited. + The format of this identifier is implementation dependent for now. + + * If a client leaves the group chat room, they MUST be ejected from the + conference. If a client leaves the 1:1 room with the conference focus user, + they SHOULD be ejected from the conference. + + * For now, rooms can contain multiple conference focus users - it's left to + user or client implementation to select which to converge on. In future this + could be mediated using a state event (e.g. `im.vector.call.mcu`), but we + can't do that right now as by default normal users can't set arbitrary state + events on a room. + + * To participate in the conference, other clients initiates a standard 1:1 + voice or video call to the conference focus user. + + * For best UX, clients SHOULD show the ongoing voice/video call in the UI + context of the group room rather than 1:1 with the focus user. If a client + recognises a conference user present in the room, it MAY chose to highlight + this in the UI (e.g. with a "conference ongoing" notification, to aid + discovery). Clients MAY hide the 1:1 room with the focus user (although in + future this room could be used for floor control or other direct + communication with the conference focus) + + * When all users have left the conference, the 'conference focus' user SHOULD + leave the room. + + * If a conference focus user joins a room but does not receive a 1:1 voice or + video call, it SHOULD time out after a period of time and leave the room. From 7a50166dc67acaf009a4d8cc0502c1e39c4b11db Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 17 Sep 2015 11:37:56 +0100 Subject: [PATCH 15/17] Move the 'thumbnail' video to the top-left of the screen This was originally laid out at the MatrixChat level which could then be CSSified, but Matthew suggests this looks a lot better being at the RoomList level above recents. Move the rendering logic to RoomList. --- skins/base/views/organisms/RoomList.js | 8 ++++++- skins/base/views/pages/MatrixChat.js | 18 +-------------- src/controllers/organisms/RoomList.js | 32 +++++++++++++++++++++++++- src/controllers/pages/MatrixChat.js | 8 ------- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/skins/base/views/organisms/RoomList.js b/skins/base/views/organisms/RoomList.js index 82e33573..a3d02066 100644 --- a/skins/base/views/organisms/RoomList.js +++ b/skins/base/views/organisms/RoomList.js @@ -18,7 +18,7 @@ limitations under the License. var React = require('react'); var ComponentBroker = require('../../../../src/ComponentBroker'); - +var CallView = ComponentBroker.get('molecules/voip/CallView'); var RoomDropTarget = ComponentBroker.get('molecules/RoomDropTarget'); var RoomListController = require("../../../../src/controllers/organisms/RoomList"); @@ -28,8 +28,14 @@ module.exports = React.createClass({ mixins: [RoomListController], render: function() { + var callElement; + if (this.state.show_call_element) { + callElement = + } + return (
+ {callElement}

Favourites

diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js index 602e241e..0fb627ab 100644 --- a/skins/base/views/pages/MatrixChat.js +++ b/skins/base/views/pages/MatrixChat.js @@ -18,7 +18,6 @@ limitations under the License. var React = require('react'); var ComponentBroker = require('../../../../src/ComponentBroker'); -var CallHandler = require('../../../../src/CallHandler'); var LeftPanel = ComponentBroker.get('organisms/LeftPanel'); var RoomView = ComponentBroker.get('organisms/RoomView'); @@ -30,7 +29,6 @@ var CreateRoom = ComponentBroker.get('organisms/CreateRoom'); var RoomDirectory = ComponentBroker.get('organisms/RoomDirectory'); var MatrixToolbar = ComponentBroker.get('molecules/MatrixToolbar'); var Notifier = ComponentBroker.get('organisms/Notifier'); -var CallView = ComponentBroker.get('molecules/voip/CallView'); var MatrixChatController = require('../../../../src/controllers/pages/MatrixChat'); @@ -55,7 +53,7 @@ module.exports = React.createClass({ render: function() { if (this.state.logged_in && this.state.ready) { - var page_element, call_element; + var page_element; var right_panel = ""; switch (this.state.page_type) { @@ -76,18 +74,6 @@ module.exports = React.createClass({ right_panel = break; } - // if we aren't viewing a room with an ongoing call, but there is an - // active call, show the call element - we need to do this to make - // audio/video not crap out - if (this.state.active_call && ( - !this.state.currentRoom || - !CallHandler.getCallForRoom(this.state.currentRoom))) { - console.log( - "Creating global CallView for active call in room %s", - this.state.active_call.roomId - ); - call_element = - } // TODO: Fix duplication here and do conditionals like we do above if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { @@ -96,7 +82,6 @@ module.exports = React.createClass({
- {call_element}
{page_element}
@@ -109,7 +94,6 @@ module.exports = React.createClass({ return (
- {call_element}
{page_element}
diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index da571552..3933f53e 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -19,9 +19,11 @@ limitations under the License. var React = require("react"); var MatrixClientPeg = require("../../MatrixClientPeg"); var RoomListSorter = require("../../RoomListSorter"); +var dis = require("../../dispatcher"); var ComponentBroker = require('../../ComponentBroker'); var ConferenceHandler = require("../../ConferenceHandler"); +var CallHandler = require("../../CallHandler"); var RoomTile = ComponentBroker.get("molecules/RoomTile"); @@ -41,7 +43,22 @@ module.exports = { }); }, + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + + onAction: function(payload) { + switch (payload.action) { + // listen for call state changes to prod the render method, which + // may hide the global CallView if the call it is tracking is dead + case 'call_state': + this._recheckCallElement(this.props.selectedRoom); + break; + } + }, + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); @@ -51,6 +68,7 @@ module.exports = { componentWillReceiveProps: function(newProps) { this.state.activityMap[newProps.selectedRoom] = undefined; + this._recheckCallElement(newProps.selectedRoom); this.setState({ activityMap: this.state.activityMap }); @@ -122,6 +140,18 @@ module.exports = { ); }, + _recheckCallElement: function(selectedRoomId) { + // if we aren't viewing a room with an ongoing call, but there is an + // active call, show the call element - we need to do this to make + // audio/video not crap out + var activeCall = CallHandler.getAnyActiveCall(); + var callForRoom = CallHandler.getCallForRoom(selectedRoomId); + var showCall = (activeCall && !callForRoom); + this.setState({ + show_call_element: showCall + }); + }, + makeRoomTiles: function() { var self = this; return this.state.roomList.map(function(room) { @@ -136,5 +166,5 @@ module.exports = { /> ); }); - }, + } }; diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index b98f42c0..08cc652d 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -26,7 +26,6 @@ var dis = require("../../dispatcher"); var q = require("q"); var ComponentBroker = require('../../ComponentBroker'); -var CallHandler = require("../../CallHandler"); var Notifier = ComponentBroker.get('organisms/Notifier'); var MatrixTools = require('../../MatrixTools'); @@ -206,13 +205,6 @@ module.exports = { case 'notifier_enabled': this.forceUpdate(); break; - case 'call_state': - // listen for call state changes to prod the render method, which - // may hide the global CallView if the call it is tracking is dead - this.setState({ - active_call: CallHandler.getAnyActiveCall() - }); - break; } }, From 240d5502fe587ab9c92fce6204819bc0bf1a7ab1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 17 Sep 2015 11:47:42 +0100 Subject: [PATCH 16/17] Add a FIXME explaining the situation around alternative FS ASes --- src/ConferenceHandler.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ConferenceHandler.js b/src/ConferenceHandler.js index 4f136806..c617672e 100644 --- a/src/ConferenceHandler.js +++ b/src/ConferenceHandler.js @@ -3,6 +3,10 @@ var q = require("q"); var Matrix = require("matrix-js-sdk"); var Room = Matrix.Room; +// FIXME: This currently forces Vector to try to hit the matrix.org AS for conferencing. +// This is bad because it prevents people running their own ASes from being used. +// This isn't permanent and will be customisable in the future: see the proposal +// at docs/conferencing.md for more info. var USER_PREFIX = "fs_"; var DOMAIN = "matrix.org"; From 56c5f6f46e5ea51f279b4f2f7b5c32e07db30919 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 19 Sep 2015 20:17:45 +0100 Subject: [PATCH 17/17] clarify deployment --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ea086ccd..59182102 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ react from your app) you must be sure to: from matrix-react-sdk's `node_modules` folder, otherwise browserify will pull in both copies of react which causes the app to break. +Deployment +========== + +Just run `npm build` in the `examples/vector` directory, and then mount the +`examples/vector` directory on your webserver to actually serve up the app, +which is entirely static content. + How to customise the SDK ========================