diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index 705a5a07f2..ad69d297fa 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -27,7 +27,8 @@ Typically, this service will store events or broadcast them to any listeners
if typically all the $on method would do is update its own $scope.
*/
angular.module('eventHandlerService', [])
-.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
+.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence',
+function(matrixService, $rootScope, $q, $timeout, mPresence) {
var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
var MSG_EVENT = "MSG_EVENT";
var MEMBER_EVENT = "MEMBER_EVENT";
@@ -38,6 +39,51 @@ angular.module('eventHandlerService', [])
var TOPIC_EVENT = "TOPIC_EVENT";
var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted
+ // used for dedupping events - could be expanded in future...
+ // FIXME: means that we leak memory over time (along with lots of the rest
+ // of the app, given we never try to reap memory yet)
+ var eventMap = {};
+
+ $rootScope.presence = {};
+
+ // TODO: This is attached to the rootScope so .html can just go containsBingWord
+ // for determining classes so it is easy to highlight bing messages. It seems a
+ // bit strange to put the impl in this service though, but I can't think of a better
+ // file to put it in.
+ $rootScope.containsBingWord = function(content) {
+ if (!content || $.type(content) != "string") {
+ return false;
+ }
+ var bingWords = matrixService.config().bingWords;
+ var shouldBing = false;
+
+ // case-insensitive name check for user_id OR display_name if they exist
+ var myUserId = matrixService.config().user_id;
+ if (myUserId) {
+ myUserId = myUserId.toLocaleLowerCase();
+ }
+ var myDisplayName = matrixService.config().display_name;
+ if (myDisplayName) {
+ myDisplayName = myDisplayName.toLocaleLowerCase();
+ }
+ if ( (myDisplayName && content.toLocaleLowerCase().indexOf(myDisplayName) != -1) ||
+ (myUserId && content.toLocaleLowerCase().indexOf(myUserId) != -1) ) {
+ shouldBing = true;
+ }
+
+ // bing word list check
+ if (bingWords && !shouldBing) {
+ for (var i=0; i<bingWords.length; i++) {
+ var re = RegExp(bingWords[i]);
+ if (content.search(re) != -1) {
+ shouldBing = true;
+ break;
+ }
+ }
+ }
+ return shouldBing;
+ };
+
var initialSyncDeferred;
var reset = function() {
@@ -46,26 +92,24 @@ angular.module('eventHandlerService', [])
$rootScope.events = {
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
};
- }
- reset();
- // used for dedupping events - could be expanded in future...
- // FIXME: means that we leak memory over time (along with lots of the rest
- // of the app, given we never try to reap memory yet)
- var eventMap = {};
+ $rootScope.presence = {};
+
+ eventMap = {};
+ };
+ reset();
- $rootScope.presence = {};
-
var initRoom = function(room_id) {
if (!(room_id in $rootScope.events.rooms)) {
console.log("Creating new handler entry for " + room_id);
- $rootScope.events.rooms[room_id] = {};
- $rootScope.events.rooms[room_id].messages = [];
- $rootScope.events.rooms[room_id].members = {};
-
- // Pagination information
- $rootScope.events.rooms[room_id].pagination = {
- earliest_token: "END" // how far back we've paginated
+ $rootScope.events.rooms[room_id] = {
+ room_id: room_id,
+ messages: [],
+ members: {},
+ // Pagination information
+ pagination: {
+ earliest_token: "END" // how far back we've paginated
+ }
};
}
};
@@ -132,6 +176,48 @@ angular.module('eventHandlerService', [])
else {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
+
+ if (window.Notification && event.user_id != matrixService.config().user_id) {
+ var shouldBing = $rootScope.containsBingWord(event.content.body);
+
+ // TODO: Binging every message when idle doesn't make much sense. Can we use this more sensibly?
+ // Unfortunately document.hidden = false on ubuntu chrome if chrome is minimised / does not have focus;
+ // true when you swap tabs though. However, for the case where the chat screen is OPEN and there is
+ // another window on top, we want to be notifying for those events. This DOES mean that there will be
+ // notifications when currently viewing the chat screen though, but that is preferable to the alternative imo.
+ var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState());
+
+ // always bing if there are 0 bing words... apparently.
+ var bingWords = matrixService.config().bingWords;
+ if (bingWords && bingWords.length === 0) {
+ shouldBing = true;
+ }
+
+ if (shouldBing) {
+ console.log("Displaying notification for "+JSON.stringify(event));
+ var member = $rootScope.events.rooms[event.room_id].members[event.user_id];
+ var displayname = undefined;
+ if (member) {
+ displayname = member.displayname;
+ }
+
+ var message = event.content.body;
+ if (event.content.msgtype === "m.emote") {
+ message = "* " + displayname + " " + message;
+ }
+
+ var notification = new window.Notification(
+ (displayname || event.user_id) +
+ " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
+ {
+ "body": message,
+ "icon": member ? member.avatar_url : undefined
+ });
+ $timeout(function() {
+ notification.close();
+ }, 5 * 1000);
+ }
+ }
}
else {
$rootScope.events.rooms[event.room_id].messages.unshift(event);
@@ -157,8 +243,9 @@ angular.module('eventHandlerService', [])
// add membership changes as if they were a room message if something interesting changed
// Exception: Do not do this if the event is a room state event because such events already come
// as room messages events. Moreover, when they come as room messages events, they are relatively ordered
- // with other other room messages
- if (event.content.prev !== event.content.membership && !isStateEvent) {
+ // with other other room messages XXX This is no longer true, you only get a single event, not a room message event.
+ // FIXME: This possibly reintroduces multiple join messages.
+ if (event.content.prev !== event.content.membership) { // && !isStateEvent
if (isLiveEvent) {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
@@ -204,7 +291,7 @@ angular.module('eventHandlerService', [])
var handleCallEvent = function(event, isLiveEvent) {
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
- if (event.type == 'm.call.invite') {
+ if (event.type === 'm.call.invite') {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
};
@@ -231,7 +318,7 @@ angular.module('eventHandlerService', [])
}
}
return index;
- }
+ };
return {
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
@@ -253,7 +340,9 @@ angular.module('eventHandlerService', [])
// FIXME: /initialSync on a particular room is not yet available
// So initRoom on a new room is not called. Make sure the room data is initialised here
- initRoom(event.room_id);
+ if (event.room_id) {
+ initRoom(event.room_id);
+ }
// Avoid duplicated events
// Needed for rooms where initialSync has not been done.
@@ -287,6 +376,7 @@ angular.module('eventHandlerService', [])
handleMessage(event, isLiveEvent);
break;
case "m.room.member":
+ isStateEvent = true;
handleRoomMember(event, isLiveEvent, isStateEvent);
break;
case "m.presence":
@@ -316,19 +406,39 @@ angular.module('eventHandlerService', [])
// isLiveEvents determines whether notifications should be shown, whether
// messages get appended to the start/end of lists, etc.
handleEvents: function(events, isLiveEvents, isStateEvents) {
+ // XXX FIXME TODO: isStateEvents is being left as undefined sometimes. It makes no sense
+ // to have isStateEvents as an arg, since things like m.room.member are ALWAYS state events.
for (var i=0; i<events.length; i++) {
this.handleEvent(events[i], isLiveEvents, isStateEvents);
}
},
// Handle messages from /initialSync or /messages
- handleRoomMessages: function(room_id, messages, isLiveEvents) {
+ handleRoomMessages: function(room_id, messages, isLiveEvents, dir) {
initRoom(room_id);
- this.handleEvents(messages.chunk, isLiveEvents);
- // Store how far back we've paginated
- // This assumes the paginations requests are contiguous and in reverse chronological order
- $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
+ var events = messages.chunk;
+
+ // Handles messages according to their time order
+ if (dir && 'b' === dir) {
+ // paginateBackMessages requests messages to be in reverse chronological order
+ for (var i=0; i<events.length; i++) {
+ // FIXME: Being live != being state
+ this.handleEvent(events[i], isLiveEvents, isLiveEvents);
+ }
+
+ // Store how far back we've paginated
+ $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
+ }
+ else {
+ // InitialSync returns messages in chronological order
+ for (var i=events.length - 1; i>=0; i--) {
+ // FIXME: Being live != being state
+ this.handleEvent(events[i], isLiveEvents, isLiveEvents);
+ }
+ // Store where to start pagination
+ $rootScope.events.rooms[room_id].pagination.earliest_token = messages.start;
+ }
},
handleInitialSyncDone: function(initialSyncData) {
@@ -343,6 +453,82 @@ angular.module('eventHandlerService', [])
resetRoomMessages: function(room_id) {
resetRoomMessages(room_id);
+ },
+
+ /**
+ * Return the last message event of a room
+ * @param {String} room_id the room id
+ * @param {Boolean} filterFake true to not take into account fake messages
+ * @returns {undefined | Event} the last message event if available
+ */
+ getLastMessage: function(room_id, filterEcho) {
+ var lastMessage;
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ for (var i = room.messages.length - 1; i >= 0; i--) {
+ var message = room.messages[i];
+
+ if (!filterEcho || undefined === message.echo_msg_state) {
+ lastMessage = message;
+ break;
+ }
+ }
+ }
+
+ return lastMessage;
+ },
+
+ /**
+ * Compute the room users number, ie the number of members who has joined the room.
+ * @param {String} room_id the room id
+ * @returns {undefined | Number} the room users number if available
+ */
+ getUsersCountInRoom: function(room_id) {
+ var memberCount;
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ memberCount = 0;
+
+ for (var i in room.members) {
+ var member = room.members[i];
+
+ if ("join" === member.membership) {
+ memberCount = memberCount + 1;
+ }
+ }
+ }
+
+ return memberCount;
+ },
+
+ /**
+ * Get the member object of a room member
+ * @param {String} room_id the room id
+ * @param {String} user_id the id of the user
+ * @returns {undefined | Object} the member object of this user in this room if he is part of the room
+ */
+ getMember: function(room_id, user_id) {
+ var member;
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ member = room.members[user_id];
+ }
+ return member;
+ },
+
+ setRoomVisibility: function(room_id, visible) {
+ if (!visible) {
+ return;
+ }
+ initRoom(room_id);
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ room.visibility = visible;
+ }
}
};
}]);
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 03b805213d..5af1ab2911 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -104,8 +104,10 @@ angular.module('eventStreamService', [])
settings.isActive = true;
var deferred = $q.defer();
- // Initial sync: get all information and the last message of all rooms of the user
- matrixService.initialSync(1, false).then(
+ // Initial sync: get all information and the last 30 messages of all rooms of the user
+ // 30 messages should be enough to display a full page of messages in a room
+ // without requiring to make an additional request
+ matrixService.initialSync(30, false).then(
function(response) {
var rooms = response.data.rooms;
for (var i = 0; i < rooms.length; ++i) {
@@ -118,6 +120,8 @@ angular.module('eventStreamService', [])
if ("state" in room) {
eventHandlerService.handleEvents(room.state, false, true);
}
+
+ eventHandlerService.setRoomVisibility(room.room_id, room.visibility);
}
var presence = response.data.presence;
diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js
index 2e3e2b0967..2ecb8b05ff 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/webclient/components/matrix/matrix-call.js
@@ -47,13 +47,19 @@ angular.module('MatrixCall', [])
this.call_id = "c" + new Date().getTime();
this.state = 'fledgling';
this.didConnect = false;
+
+ // a queue for candidates waiting to go out. We try to amalgamate candidates into a single candidate message where possible
+ this.candidateSendQueue = [];
+ this.candidateSendTries = 0;
}
+ MatrixCall.CALL_TIMEOUT = 60000;
+
MatrixCall.prototype.createPeerConnection = function() {
var stunServer = 'stun:stun.l.google.com:19302';
var pc;
if (window.mozRTCPeerConnection) {
- pc = window.mozRTCPeerConnection({'url': stunServer});
+ pc = new window.mozRTCPeerConnection({'url': stunServer});
} else {
pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]});
}
@@ -74,12 +80,30 @@ angular.module('MatrixCall', [])
this.config = config;
};
- MatrixCall.prototype.initWithInvite = function(msg) {
- this.msg = msg;
+ MatrixCall.prototype.initWithInvite = function(event) {
+ this.msg = event.content;
this.peerConn = this.createPeerConnection();
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
this.state = 'ringing';
this.direction = 'inbound';
+ var self = this;
+ $timeout(function() {
+ if (self.state == 'ringing') {
+ self.state = 'ended';
+ self.hangupParty = 'remote'; // effectively
+ self.stopAllMedia();
+ if (self.peerConn.signalingState != 'closed') self.peerConn.close();
+ if (self.onHangup) self.onHangup(self);
+ }
+ }, this.msg.lifetime - event.age);
+ };
+
+ // perverse as it may seem, sometimes we want to instantiate a call with a hangup message
+ // (because when getting the state of the room on load, events come in reverse order and
+ // we want to remember that a call has been hung up)
+ MatrixCall.prototype.initWithHangup = function(event) {
+ this.msg = event.content;
+ this.state = 'ended';
};
MatrixCall.prototype.answer = function() {
@@ -174,12 +198,7 @@ angular.module('MatrixCall', [])
MatrixCall.prototype.gotLocalIceCandidate = function(event) {
console.log(event);
if (event.candidate) {
- var content = {
- version: 0,
- call_id: this.call_id,
- candidate: event.candidate
- };
- this.sendEventWithRetry('m.call.candidate', content);
+ this.sendCandidate(event.candidate);
}
}
@@ -189,14 +208,12 @@ angular.module('MatrixCall', [])
console.log("Ignoring remote ICE candidate because call has ended");
return;
}
- var candidateObject = new RTCIceCandidate({
- sdpMLineIndex: cand.label,
- candidate: cand.candidate
- });
- this.peerConn.addIceCandidate(candidateObject, function() {}, function(e) {});
+ this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {});
};
MatrixCall.prototype.receivedAnswer = function(msg) {
+ if (this.state == 'ended') return;
+
this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
this.state = 'connecting';
};
@@ -214,11 +231,19 @@ angular.module('MatrixCall', [])
var content = {
version: 0,
call_id: this.call_id,
- offer: description
+ offer: description,
+ lifetime: MatrixCall.CALL_TIMEOUT
};
this.sendEventWithRetry('m.call.invite', content);
var self = this;
+ $timeout(function() {
+ if (self.state == 'invite_sent') {
+ self.hangupReason = 'invite_timeout';
+ self.hangup();
+ }
+ }, MatrixCall.CALL_TIMEOUT);
+
$rootScope.$apply(function() {
self.state = 'invite_sent';
});
@@ -370,5 +395,53 @@ angular.module('MatrixCall', [])
}, delayMs);
};
+ // Sends candidates with are sent in a special way because we try to amalgamate them into one message
+ MatrixCall.prototype.sendCandidate = function(content) {
+ this.candidateSendQueue.push(content);
+ var self = this;
+ if (this.candidateSendTries == 0) $timeout(function() { self.sendCandidateQueue(); }, 100);
+ };
+
+ MatrixCall.prototype.sendCandidateQueue = function(content) {
+ if (this.candidateSendQueue.length == 0) return;
+
+ var cands = this.candidateSendQueue;
+ this.candidateSendQueue = [];
+ ++this.candidateSendTries;
+ var content = {
+ version: 0,
+ call_id: this.call_id,
+ candidates: cands
+ };
+ var self = this;
+ console.log("Attempting to send "+cands.length+" candidates");
+ matrixService.sendEvent(self.room_id, 'm.call.candidates', undefined, content).then(function() { self.candsSent(); }, function(error) { self.candsSendFailed(cands, error); } );
+ };
+
+ MatrixCall.prototype.candsSent = function() {
+ this.candidateSendTries = 0;
+ this.sendCandidateQueue();
+ };
+
+ MatrixCall.prototype.candsSendFailed = function(cands, error) {
+ for (var i = 0; i < cands.length; ++i) {
+ this.candidateSendQueue.push(cands[i]);
+ }
+
+ if (this.candidateSendTries > 5) {
+ console.log("Failed to send candidates on attempt "+ev.tries+". Giving up for now.");
+ this.candidateSendTries = 0;
+ return;
+ }
+
+ var delayMs = 500 * Math.pow(2, this.candidateSendTries);
+ ++this.candidateSendTries;
+ console.log("Failed to send candidates. Retrying in "+delayMs+"ms");
+ var self = this;
+ $timeout(function() {
+ self.sendCandidateQueue();
+ }, delayMs);
+ };
+
return MatrixCall;
}]);
diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js
index 015a88bcad..8b168cdedb 100644
--- a/webclient/components/matrix/matrix-filter.js
+++ b/webclient/components/matrix/matrix-filter.js
@@ -26,72 +26,74 @@ angular.module('matrixFilter', [])
// If there is an alias, use it
// TODO: only one alias is managed for now
var alias = matrixService.getRoomIdToAliasMapping(room_id);
- if (alias) {
- roomName = alias;
- }
-
- if (undefined === roomName) {
- var room = $rootScope.events.rooms[room_id];
- if (room) {
- // Get name from room state date
- var room_name_event = room["m.room.name"];
- if (room_name_event) {
- roomName = room_name_event.content.name;
- }
- else if (room.members) {
- // Else, build the name from its users
- // FIXME: Is it still required?
- // Limit the room renaming to 1:1 room
- if (2 === Object.keys(room.members).length) {
- for (var i in room.members) {
- var member = room.members[i];
- if (member.state_key !== matrixService.config().user_id) {
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ // Get name from room state date
+ var room_name_event = room["m.room.name"];
+ if (room_name_event) {
+ roomName = room_name_event.content.name;
+ }
+ else if (alias) {
+ roomName = alias;
+ }
+ else if (room.members) {
+ // Else, build the name from its users
+ // FIXME: Is it still required?
+ // Limit the room renaming to 1:1 room
+ if (2 === Object.keys(room.members).length) {
+ for (var i in room.members) {
+ var member = room.members[i];
+ if (member.state_key !== matrixService.config().user_id) {
- if (member.state_key in $rootScope.presence) {
- // If the user is available in presence, use the displayname there
- // as it is the most uptodate
- roomName = $rootScope.presence[member.state_key].content.displayname;
- }
- else if (member.content.displayname) {
- roomName = member.content.displayname;
- }
- else {
- roomName = member.state_key;
- }
+ if (member.state_key in $rootScope.presence) {
+ // If the user is available in presence, use the displayname there
+ // as it is the most uptodate
+ roomName = $rootScope.presence[member.state_key].content.displayname;
}
- }
- }
- else if (1 === Object.keys(room.members).length) {
- // The other member may be in the invite list, get all invited users
- var invitedUserIDs = [];
- for (var i in room.messages) {
- var message = room.messages[i];
- if ("m.room.member" === message.type && "invite" === message.membership) {
- // Make sure there is no duplicate user
- if (-1 === invitedUserIDs.indexOf(message.state_key)) {
- invitedUserIDs.push(message.state_key);
- }
- }
- }
-
- // For now, only 1:1 room needs to be renamed. It means only 1 invited user
- if (1 === invitedUserIDs.length) {
- var userID = invitedUserIDs[0];
-
- // Try to resolve his displayname in presence global data
- if (userID in $rootScope.presence) {
- roomName = $rootScope.presence[userID].content.displayname;
+ else if (member.content.displayname) {
+ roomName = member.content.displayname;
}
else {
- roomName = userID;
+ roomName = member.state_key;
}
}
}
}
+ else if (1 === Object.keys(room.members).length) {
+ // The other member may be in the invite list, get all invited users
+ var invitedUserIDs = [];
+ for (var i in room.messages) {
+ var message = room.messages[i];
+ if ("m.room.member" === message.type && "invite" === message.membership) {
+ // Make sure there is no duplicate user
+ if (-1 === invitedUserIDs.indexOf(message.state_key)) {
+ invitedUserIDs.push(message.state_key);
+ }
+ }
+ }
+
+ // For now, only 1:1 room needs to be renamed. It means only 1 invited user
+ if (1 === invitedUserIDs.length) {
+ var userID = invitedUserIDs[0];
+
+ // Try to resolve his displayname in presence global data
+ if (userID in $rootScope.presence) {
+ roomName = $rootScope.presence[userID].content.displayname;
+ }
+ else {
+ roomName = userID;
+ }
+ }
+ }
}
}
+ // Always show the alias in the room displayed name
+ if (roomName && alias && alias !== roomName) {
+ roomName += " (" + alias + ")";
+ }
+
if (undefined === roomName) {
// By default, use the room ID
roomName = room_id;
diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js
index b0dcf19100..d05eecf72a 100644
--- a/webclient/components/matrix/matrix-phone-service.js
+++ b/webclient/components/matrix/matrix-phone-service.js
@@ -24,22 +24,52 @@ angular.module('matrixPhoneService', [])
matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT";
matrixPhoneService.allCalls = {};
+ // a place to save candidates that come in for calls we haven't got invites for yet (when paginating backwards)
+ matrixPhoneService.candidatesByCall = {};
matrixPhoneService.callPlaced = function(call) {
matrixPhoneService.allCalls[call.call_id] = call;
};
$rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
- if (!isLive) return; // until matrix supports expiring messages
if (event.user_id == matrixService.config().user_id) return;
+
var msg = event.content;
+
if (event.type == 'm.call.invite') {
+ if (event.age == undefined || msg.lifetime == undefined) {
+ // if the event doesn't have either an age (the HS is too old) or a lifetime
+ // (the sending client was too old when it sent it) then fall back to old behaviour
+ if (!isLive) return; // until matrix supports expiring messages
+ }
+
+ if (event.age > msg.lifetime) {
+ console.log("Ignoring expired call event of type "+event.type);
+ return;
+ }
+
+ var call = undefined;
+ if (!isLive) {
+ // if this event wasn't live then this call may already be over
+ call = matrixPhoneService.allCalls[msg.call_id];
+ if (call && call.state == 'ended') {
+ return;
+ }
+ }
+
var MatrixCall = $injector.get('MatrixCall');
var call = new MatrixCall(event.room_id);
call.call_id = msg.call_id;
- call.initWithInvite(msg);
+ call.initWithInvite(event);
matrixPhoneService.allCalls[call.call_id] = call;
+ // if we stashed candidate events for that call ID, play them back now
+ if (!isLive && matrixPhoneService.candidatesByCall[call.call_id] != undefined) {
+ for (var i = 0; i < matrixPhoneService.candidatesByCall[call.call_id].length; ++i) {
+ call.gotRemoteIceCandidate(matrixPhoneService.candidatesByCall[call.call_id][i]);
+ }
+ }
+
// Were we trying to call that user (room)?
var existingCall;
var callIds = Object.keys(matrixPhoneService.allCalls);
@@ -77,21 +107,37 @@ angular.module('matrixPhoneService', [])
return;
}
call.receivedAnswer(msg);
- } else if (event.type == 'm.call.candidate') {
+ } else if (event.type == 'm.call.candidates') {
var call = matrixPhoneService.allCalls[msg.call_id];
- if (!call) {
- console.log("Got candidate for unknown call ID "+msg.call_id);
+ if (!call && isLive) {
+ console.log("Got candidates for unknown call ID "+msg.call_id);
return;
+ } else if (!call) {
+ if (matrixPhoneService.candidatesByCall[msg.call_id] == undefined) {
+ matrixPhoneService.candidatesByCall[msg.call_id] = [];
+ }
+ matrixPhoneService.candidatesByCall[msg.call_id] = matrixPhoneService.candidatesByCall[msg.call_id].concat(msg.candidates);
+ } else {
+ for (var i = 0; i < msg.candidates.length; ++i) {
+ call.gotRemoteIceCandidate(msg.candidates[i]);
+ }
}
- call.gotRemoteIceCandidate(msg.candidate);
} else if (event.type == 'm.call.hangup') {
var call = matrixPhoneService.allCalls[msg.call_id];
- if (!call) {
+ if (!call && isLive) {
console.log("Got hangup for unknown call ID "+msg.call_id);
- return;
+ } else if (!call) {
+ // if not live, store the fact that the call has ended because we're probably getting events backwards so
+ // the hangup will come before the invite
+ var MatrixCall = $injector.get('MatrixCall');
+ var call = new MatrixCall(event.room_id);
+ call.call_id = msg.call_id;
+ call.initWithHangup(event);
+ matrixPhoneService.allCalls[msg.call_id] = call;
+ } else {
+ call.onHangupReceived();
+ delete(matrixPhoneService.allCalls[msg.call_id]);
}
- call.onHangupReceived();
- delete(matrixPhoneService.allCalls[msg.call_id]);
}
});
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index 68ef16800b..069e02e939 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -81,38 +81,155 @@ angular.module('matrixService', [])
return $http(request);
};
+
+ var doRegisterLogin = function(path, loginType, sessionId, userName, password, threepidCreds) {
+ var data = {};
+ if (loginType === "m.login.recaptcha") {
+ var challengeToken = Recaptcha.get_challenge();
+ var captchaEntry = Recaptcha.get_response();
+ data = {
+ type: "m.login.recaptcha",
+ challenge: challengeToken,
+ response: captchaEntry
+ };
+ }
+ else if (loginType === "m.login.email.identity") {
+ data = {
+ threepidCreds: threepidCreds
+ };
+ }
+ else if (loginType === "m.login.password") {
+ data = {
+ user: userName,
+ password: password
+ };
+ }
+
+ if (sessionId) {
+ data.session = sessionId;
+ }
+ data.type = loginType;
+ console.log("doRegisterLogin >>> " + loginType);
+ return doRequest("POST", path, undefined, data);
+ };
return {
/****** Home server API ******/
prefix: prefixPath,
// Register an user
- register: function(user_name, password, threepidCreds, useCaptcha) {
- // The REST path spec
+ register: function(user_name, password, threepidCreds, useCaptcha) {
+ // registration is composed of multiple requests, to check you can
+ // register, then to actually register. This deferred will fire when
+ // all the requests are done, along with the final response.
+ var deferred = $q.defer();
var path = "/register";
- var data = {
- user_id: user_name,
- password: password,
- threepidCreds: threepidCreds
- };
+ // check we can actually register with this HS.
+ doRequest("GET", path, undefined, undefined).then(
+ function(response) {
+ console.log("/register [1] : "+JSON.stringify(response));
+ var flows = response.data.flows;
+ var knownTypes = [
+ "m.login.password",
+ "m.login.recaptcha",
+ "m.login.email.identity"
+ ];
+ // if they entered 3pid creds, we want to use a flow which uses it.
+ var useThreePidFlow = threepidCreds != undefined;
+ var flowIndex = 0;
+ var firstRegType = undefined;
+
+ for (var i=0; i<flows.length; i++) {
+ var isThreePidFlow = false;
+ if (flows[i].stages) {
+ for (var j=0; j<flows[i].stages.length; j++) {
+ var regType = flows[i].stages[j];
+ if (knownTypes.indexOf(regType) === -1) {
+ deferred.reject("Unknown type: "+regType);
+ return;
+ }
+ if (regType == "m.login.email.identity") {
+ isThreePidFlow = true;
+ }
+ if (!useCaptcha && regType == "m.login.recaptcha") {
+ console.error("Web client setup to not use captcha, but HS demands a captcha.");
+ deferred.reject({
+ data: {
+ errcode: "M_CAPTCHA_NEEDED",
+ error: "Home server requires a captcha."
+ }
+ });
+ return;
+ }
+ }
+ }
+
+ if ( (isThreePidFlow && useThreePidFlow) || (!isThreePidFlow && !useThreePidFlow) ) {
+ flowIndex = i;
+ }
+
+ if (knownTypes.indexOf(flows[i].type) == -1) {
+ deferred.reject("Unknown type: "+flows[i].type);
+ return;
+ }
+ }
+
+ // looks like we can register fine, go ahead and do it.
+ console.log("Using flow " + JSON.stringify(flows[flowIndex]));
+ firstRegType = flows[flowIndex].type;
+ var sessionId = undefined;
+
+ // generic response processor so it can loop as many times as required
+ var loginResponseFunc = function(response) {
+ if (response.data.session) {
+ sessionId = response.data.session;
+ }
+ console.log("login response: " + JSON.stringify(response.data));
+ if (response.data.access_token) {
+ deferred.resolve(response);
+ }
+ else if (response.data.next) {
+ var nextType = response.data.next;
+ if (response.data.next instanceof Array) {
+ for (var i=0; i<response.data.next.length; i++) {
+ if (useThreePidFlow && response.data.next[i] == "m.login.email.identity") {
+ nextType = response.data.next[i];
+ break;
+ }
+ else if (!useThreePidFlow && response.data.next[i] != "m.login.email.identity") {
+ nextType = response.data.next[i];
+ break;
+ }
+ }
+ }
+ return doRegisterLogin(path, nextType, sessionId, user_name, password, threepidCreds).then(
+ loginResponseFunc,
+ function(err) {
+ deferred.reject(err);
+ }
+ );
+ }
+ else {
+ deferred.reject("Unknown continuation: "+JSON.stringify(response));
+ }
+ };
+
+ // set the ball rolling
+ doRegisterLogin(path, firstRegType, undefined, user_name, password, threepidCreds).then(
+ loginResponseFunc,
+ function(err) {
+ deferred.reject(err);
+ }
+ );
+
+ },
+ function(err) {
+ deferred.reject(err);
+ }
+ );
- if (useCaptcha) {
- // Not all home servers will require captcha on signup, but if this flag is checked,
- // send captcha information.
- // TODO: Might be nice to make this a bit more flexible..
- var challengeToken = Recaptcha.get_challenge();
- var captchaEntry = Recaptcha.get_response();
- var captchaType = "m.login.recaptcha";
-
- data.captcha = {
- type: captchaType,
- challenge: challengeToken,
- response: captchaEntry
- };
- }
-
- return doRequest("POST", path, undefined, data);
+ return deferred.promise;
},
// Create a room
|