mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 09:15:41 +03:00
Merge pull request #160 from vector-im/conferencing
Add conferencing support
This commit is contained in:
commit
81db1b2360
13 changed files with 423 additions and 62 deletions
52
docs/conferencing.md
Normal file
52
docs/conferencing.md
Normal file
|
@ -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.
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
|
||||
|
|
|
@ -176,6 +176,15 @@ module.exports = React.createClass({
|
|||
roomEdit = <Loader/>;
|
||||
}
|
||||
|
||||
var conferenceCallNotification = null;
|
||||
if (this.state.displayConfCallNotification) {
|
||||
conferenceCallNotification = (
|
||||
<div className="mx_RoomView_ongoingConfCallNotification" onClick={this.onConferenceNotificationClick}>
|
||||
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 }>
|
||||
|
|
|
@ -30,7 +30,7 @@ var RoomDirectory = ComponentBroker.get('organisms/RoomDirectory');
|
|||
var MatrixToolbar = ComponentBroker.get('molecules/MatrixToolbar');
|
||||
var Notifier = ComponentBroker.get('organisms/Notifier');
|
||||
|
||||
var MatrixChatController = require("../../../../src/controllers/pages/MatrixChat");
|
||||
var MatrixChatController = require('../../../../src/controllers/pages/MatrixChat');
|
||||
|
||||
// should be atomised
|
||||
var Loader = require("react-loader");
|
||||
|
@ -75,6 +75,7 @@ module.exports = React.createClass({
|
|||
break;
|
||||
}
|
||||
|
||||
// TODO: Fix duplication here and do conditionals like we do above
|
||||
if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
|
||||
return (
|
||||
<div className="mx_MatrixChat_wrapper">
|
||||
|
|
|
@ -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> ·
|
||||
<a href="https://twitter.com/@VectorCo">twitter</a> ·
|
||||
<a href="https://github.com/vector-im/vector-web">github</a> ·
|
||||
<a href="https://matrix.org">powered by Matrix</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -57,6 +57,8 @@ var MatrixClientPeg = require("./MatrixClientPeg");
|
|||
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");
|
||||
|
||||
|
@ -105,7 +107,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");
|
||||
}
|
||||
|
@ -153,7 +155,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);
|
||||
|
@ -161,40 +167,52 @@ 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 confCall = new ConferenceCall(
|
||||
MatrixClientPeg.get(), payload.room_id
|
||||
);
|
||||
confCall.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]) {
|
||||
if (module.exports.getAnyActiveCall()) {
|
||||
payload.call.hangup("busy");
|
||||
return; // don't allow >1 call to be received, hangup newer one.
|
||||
}
|
||||
|
@ -224,7 +242,40 @@ 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++) {
|
||||
if (calls[roomsWithCalls[i]] &&
|
||||
calls[roomsWithCalls[i]].call_state !== "ended") {
|
||||
return calls[roomsWithCalls[i]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
94
src/ConferenceHandler.js
Normal file
94
src/ConferenceHandler.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
"use strict";
|
||||
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";
|
||||
|
||||
function ConferenceCall(matrixClient, groupChatRoomId) {
|
||||
this.client = matrixClient;
|
||||
this.groupRoomId = groupChatRoomId;
|
||||
this.confUserId = module.exports.getConferenceUserIdForRoom(this.groupRoomId);
|
||||
}
|
||||
|
||||
ConferenceCall.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. 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;
|
||||
});
|
||||
};
|
||||
|
||||
ConferenceCall.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 member = groupRoom.getMember(this.confUserId);
|
||||
if (member && member.membership === "join") {
|
||||
return q();
|
||||
}
|
||||
return this.client.invite(this.groupRoomId, this.confUserId);
|
||||
};
|
||||
|
||||
ConferenceCall.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++) {
|
||||
var confUser = rooms[i].getMember(this.confUserId);
|
||||
if (confUser && confUser.membership === "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);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.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;
|
||||
|
|
@ -19,6 +19,9 @@ 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');
|
||||
|
@ -44,7 +47,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 = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
var callState = call ? call.call_state : "ended";
|
||||
this.setState({
|
||||
call_state: callState
|
||||
|
@ -57,15 +60,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 = CallHandler.getCallForRoom(payload.room_id);
|
||||
var callState = call ? call.call_state : "ended";
|
||||
this.setState({
|
||||
call_state: callState
|
||||
|
@ -87,9 +87,13 @@ module.exports = {
|
|||
});
|
||||
},
|
||||
onHangupClick: function() {
|
||||
var call = CallHandler.getCallForRoom(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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -40,26 +57,27 @@ 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) {
|
||||
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
|
||||
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 {
|
||||
|
|
|
@ -19,11 +19,16 @@ 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");
|
||||
|
||||
var HIDE_CONFERENCE_CHANS = true;
|
||||
|
||||
module.exports = {
|
||||
componentWillMount: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
@ -38,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);
|
||||
|
@ -48,6 +68,7 @@ module.exports = {
|
|||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.state.activityMap[newProps.selectedRoom] = undefined;
|
||||
this._recheckCallElement(newProps.selectedRoom);
|
||||
this.setState({
|
||||
activityMap: this.state.activityMap
|
||||
});
|
||||
|
@ -96,12 +117,41 @@ module.exports = {
|
|||
getRoomList: function() {
|
||||
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 me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
var shouldShowRoom = (
|
||||
me && (me.membership == "join" || me.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 !== me.userId
|
||||
})[0];
|
||||
if (ConferenceHandler.isConferenceUser(otherMember)) {
|
||||
// console.log("Hiding conference 1:1 room %s", room.roomId);
|
||||
shouldShowRoom = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return shouldShowRoom;
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_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) {
|
||||
|
@ -116,5 +166,5 @@ module.exports = {
|
|||
/>
|
||||
);
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,42 @@ 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 confMember = MatrixClientPeg.get().getRoom(this.props.roomId).getMember(
|
||||
ConferenceHandler.getConferenceUserIdForRoom(this.props.roomId)
|
||||
);
|
||||
|
||||
if (!confMember) {
|
||||
return;
|
||||
}
|
||||
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") &&
|
||||
confMember.membership === "join"
|
||||
)
|
||||
});
|
||||
},
|
||||
|
||||
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();
|
||||
|
@ -183,6 +227,7 @@ module.exports = {
|
|||
|
||||
this.fillSpace();
|
||||
}
|
||||
this._updateConfCallNotification();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
|
|
Loading…
Reference in a new issue