From 77401e215edb496e426bbf5f35986ae6c85682b2 Mon Sep 17 00:00:00 2001
From: Kegan Dougal <kegan@matrix.org>
Date: Fri, 11 Sep 2015 15:49:47 +0100
Subject: [PATCH 01/16] 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 <kegan@matrix.org>
Date: Fri, 11 Sep 2015 16:14:30 +0100
Subject: [PATCH 02/16] 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 <kegan@matrix.org>
Date: Fri, 11 Sep 2015 16:55:48 +0100
Subject: [PATCH 03/16] 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 <kegan@matrix.org>
Date: Tue, 15 Sep 2015 11:05:53 +0100
Subject: [PATCH 04/16] 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 = <RightPanel/>
                     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 = <CallView className="mx_MatrixChat_callView"/>
+            }
 
             if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
                 return (
@@ -81,6 +94,7 @@ module.exports = React.createClass({
                             <MatrixToolbar />
                             <div className="mx_MatrixChat mx_MatrixChat_toolbarShowing">
                                 <LeftPanel selectedRoom={this.state.currentRoom} />
+                                {call_element}
                                 <main className="mx_MatrixChat_middlePanel">
                                     {page_element}
                                 </main>
@@ -93,6 +107,7 @@ module.exports = React.createClass({
                 return (
                         <div className="mx_MatrixChat">
                             <LeftPanel selectedRoom={this.state.currentRoom} />
+                            {call_element}
                             <main className="mx_MatrixChat_middlePanel">
                                 {page_element}
                             </main>
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 <kegan@matrix.org>
Date: Tue, 15 Sep 2015 11:43:51 +0100
Subject: [PATCH 05/16] 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 <kegan@matrix.org>
Date: Tue, 15 Sep 2015 13:04:09 +0100
Subject: [PATCH 06/16] 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 <kegan@matrix.org>
Date: Tue, 15 Sep 2015 13:19:07 +0100
Subject: [PATCH 07/16] 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 <kegan@matrix.org>
Date: Tue, 15 Sep 2015 14:18:17 +0100
Subject: [PATCH 08/16] 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 = <Loader/>;
             }
 
+            var conferenceCallNotification = null;
+            if (this.state.displayConfCallNotification) {
+                conferenceCallNotification = (
+                    <div className="mx_RoomView_ongoingConfCallNotification">
+                        Ongoing conference call
+                    </div>
+                );
+            }
+
             var fileDropTarget = null;
             if (this.state.draggingFile) {
                 fileDropTarget = <div className="mx_RoomView_fileDropTarget">
@@ -192,6 +201,7 @@ module.exports = React.createClass({
                         onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} />
                     <div className="mx_RoomView_auxPanel">
                         <CallView room={this.state.room}/>
+                        { conferenceCallNotification }
                         { roomEdit }
                     </div>
                     <div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }>
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 = <CallView className="mx_MatrixChat_callView"/>
             }
 
+            // TODO: Fix duplication here and do conditionals like we do above
             if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
                 return (
                         <div className="mx_MatrixChat_wrapper">
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 <kegan@matrix.org>
Date: Tue, 15 Sep 2015 15:02:02 +0100
Subject: [PATCH 09/16] 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 <kegan@matrix.org>
Date: Tue, 15 Sep 2015 15:49:33 +0100
Subject: [PATCH 10/16] 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 <kegan@matrix.org>
Date: Tue, 15 Sep 2015 15:55:02 +0100
Subject: [PATCH 11/16] 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 = (
-                    <div className="mx_RoomView_ongoingConfCallNotification">
+                    <div className="mx_RoomView_ongoingConfCallNotification" onClick={this.onConferenceNotificationClick}>
                         Ongoing conference call
                     </div>
                 );
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 <matthew@matrix.org>
Date: Tue, 15 Sep 2015 17:05:49 +0100
Subject: [PATCH 12/16] 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}
                 </div>
                 <a className="mx_Login_create" onClick={this.showRegister} href="#">Create a new account</a>
+                <br/>
+                <div className="mx_Login_links">
+                    <a href="https://medium.com/@Vector">blog</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
+                    <a href="https://twitter.com/@VectorCo">twitter</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
+                    <a href="https://github.com/vector-im/vector-web">github</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
+                    <a href="https://matrix.org">powered by Matrix</a>
+                </div>
             </div>
         );
     },

From e991beb9004b009c42fba2375f4625c2082ac3eb Mon Sep 17 00:00:00 2001
From: Kegan Dougal <kegan@matrix.org>
Date: Thu, 17 Sep 2015 11:06:08 +0100
Subject: [PATCH 13/16] 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 <kegan@matrix.org>
Date: Thu, 17 Sep 2015 11:06:50 +0100
Subject: [PATCH 14/16] 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 <kegan@matrix.org>
Date: Thu, 17 Sep 2015 11:37:56 +0100
Subject: [PATCH 15/16] 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 = <CallView className="mx_MatrixChat_callView"/>
+        }
+
         return (
             <div className="mx_RoomList">
+                {callElement}
                 <h2 className="mx_RoomList_favourites_label">Favourites</h2>
                 <RoomDropTarget text="Drop here to favourite"/>
 
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 = <RightPanel/>
                     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 = <CallView className="mx_MatrixChat_callView"/>
-            }
 
             // 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({
                             <MatrixToolbar />
                             <div className="mx_MatrixChat mx_MatrixChat_toolbarShowing">
                                 <LeftPanel selectedRoom={this.state.currentRoom} />
-                                {call_element}
                                 <main className="mx_MatrixChat_middlePanel">
                                     {page_element}
                                 </main>
@@ -109,7 +94,6 @@ module.exports = React.createClass({
                 return (
                         <div className="mx_MatrixChat">
                             <LeftPanel selectedRoom={this.state.currentRoom} />
-                            {call_element}
                             <main className="mx_MatrixChat_middlePanel">
                                 {page_element}
                             </main>
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 <kegan@matrix.org>
Date: Thu, 17 Sep 2015 11:47:42 +0100
Subject: [PATCH 16/16] 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";