diff options
Diffstat (limited to 'webclient/components')
-rw-r--r-- | webclient/components/matrix/event-handler-service.js | 199 | ||||
-rw-r--r-- | webclient/components/matrix/event-stream-service.js | 15 | ||||
-rw-r--r-- | webclient/components/matrix/matrix-call.js | 226 | ||||
-rw-r--r-- | webclient/components/matrix/matrix-filter.js | 6 | ||||
-rw-r--r-- | webclient/components/matrix/matrix-phone-service.js | 41 | ||||
-rw-r--r-- | webclient/components/matrix/matrix-service.js | 31 |
6 files changed, 397 insertions, 121 deletions
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js index d2bb31053f..705a5a07f2 100644 --- a/webclient/components/matrix/event-handler-service.js +++ b/webclient/components/matrix/event-handler-service.js @@ -35,13 +35,20 @@ angular.module('eventHandlerService', []) var POWERLEVEL_EVENT = "POWERLEVEL_EVENT"; var CALL_EVENT = "CALL_EVENT"; var NAME_EVENT = "NAME_EVENT"; + var TOPIC_EVENT = "TOPIC_EVENT"; + var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted + + var initialSyncDeferred; + + var reset = function() { + initialSyncDeferred = $q.defer(); + + $rootScope.events = { + rooms: {} // will contain roomId: { messages:[], members:{userid1: event} } + }; + } + reset(); - var InitialSyncDeferred = $q.defer(); - - $rootScope.events = { - rooms: {} // will contain roomId: { messages:[], members:{userid1: event} } - }; - // 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) @@ -55,6 +62,11 @@ angular.module('eventHandlerService', []) $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 + }; } }; @@ -64,9 +76,37 @@ angular.module('eventHandlerService', []) } }; - var handleRoomCreate = function(event, isLiveEvent) { - initRoom(event.room_id); + // Generic method to handle events data + var handleRoomDateEvent = function(event, isLiveEvent, addToRoomMessages) { + // Add topic changes as if they were a room message + if (addToRoomMessages) { + if (isLiveEvent) { + $rootScope.events.rooms[event.room_id].messages.push(event); + } + else { + $rootScope.events.rooms[event.room_id].messages.unshift(event); + } + } + // live events always update, but non-live events only update if the + // ts is later. + var latestData = true; + if (!isLiveEvent) { + var eventTs = event.ts; + var storedEvent = $rootScope.events.rooms[event.room_id][event.type]; + if (storedEvent) { + if (storedEvent.ts > eventTs) { + // ignore it, we have a newer one already. + latestData = false; + } + } + } + if (latestData) { + $rootScope.events.rooms[event.room_id][event.type] = event; + } + }; + + var handleRoomCreate = function(event, isLiveEvent) { // For now, we do not use the event data. Simply signal it to the app controllers $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent); }; @@ -76,13 +116,18 @@ angular.module('eventHandlerService', []) }; var handleMessage = function(event, isLiveEvent) { - initRoom(event.room_id); - if (isLiveEvent) { if (event.user_id === matrixService.config().user_id && (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) { - // assume we've already echoed it - // FIXME: track events by ID and ungrey the right message to show it's been delivered + // Assume we've already echoed it. So, there is a fake event in the messages list of the room + // Replace this fake event by the true one + var index = getRoomEventIndex(event.room_id, event.event_id); + if (index) { + $rootScope.events.rooms[event.room_id].messages[index] = event; + } + else { + $rootScope.events.rooms[event.room_id].messages.push(event); + } } else { $rootScope.events.rooms[event.room_id].messages.push(event); @@ -100,9 +145,7 @@ angular.module('eventHandlerService', []) $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent); }; - var handleRoomMember = function(event, isLiveEvent) { - initRoom(event.room_id); - + var handleRoomMember = function(event, isLiveEvent, isStateEvent) { // if the server is stupidly re-relaying a no-op join, discard it. if (event.prev_content && event.content.membership === "join" && @@ -112,7 +155,10 @@ angular.module('eventHandlerService', []) } // add membership changes as if they were a room message if something interesting changed - if (event.content.prev !== event.content.membership) { + // 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) { if (isLiveEvent) { $rootScope.events.rooms[event.room_id].messages.push(event); } @@ -121,8 +167,13 @@ angular.module('eventHandlerService', []) } } - $rootScope.events.rooms[event.room_id].members[event.state_key] = event; - $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent); + // Use data from state event or the latest data from the stream. + // Do not care of events that come when paginating back + if (isStateEvent || isLiveEvent) { + $rootScope.events.rooms[event.room_id].members[event.state_key] = event; + } + + $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent); }; var handlePresence = function(event, isLiveEvent) { @@ -131,8 +182,6 @@ angular.module('eventHandlerService', []) }; var handlePowerLevels = function(event, isLiveEvent) { - initRoom(event.room_id); - // Keep the latest data. Do not care of events that come when paginating back if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) { $rootScope.events.rooms[event.room_id][event.type] = event; @@ -140,19 +189,50 @@ angular.module('eventHandlerService', []) } }; - var handleRoomName = function(event, isLiveEvent) { - console.log("handleRoomName " + isLiveEvent); - - initRoom(event.room_id); - - $rootScope.events.rooms[event.room_id][event.type] = event; + var handleRoomName = function(event, isLiveEvent, isStateEvent) { + console.log("handleRoomName room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - name: " + event.content.name); + handleRoomDateEvent(event, isLiveEvent, !isStateEvent); $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent); }; + + + var handleRoomTopic = function(event, isLiveEvent, isStateEvent) { + console.log("handleRoomTopic room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - topic: " + event.content.topic); + handleRoomDateEvent(event, isLiveEvent, !isStateEvent); + $rootScope.$broadcast(TOPIC_EVENT, event, isLiveEvent); + }; var handleCallEvent = function(event, isLiveEvent) { $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent); + if (event.type == 'm.call.invite') { + $rootScope.events.rooms[event.room_id].messages.push(event); + } }; + /** + * Get the index of the event in $rootScope.events.rooms[room_id].messages + * @param {type} room_id the room id + * @param {type} event_id the event id to look for + * @returns {Number | undefined} the index. undefined if not found. + */ + var getRoomEventIndex = function(room_id, event_id) { + var index; + + var room = $rootScope.events.rooms[room_id]; + if (room) { + // Start looking from the tail since the first goal of this function + // is to find a messaged among the latest ones + for (var i = room.messages.length - 1; i > 0; i--) { + var message = room.messages[i]; + if (event_id === message.event_id) { + index = i; + break; + } + } + } + return index; + } + return { ROOM_CREATE_EVENT: ROOM_CREATE_EVENT, MSG_EVENT: MSG_EVENT, @@ -161,19 +241,37 @@ angular.module('eventHandlerService', []) POWERLEVEL_EVENT: POWERLEVEL_EVENT, CALL_EVENT: CALL_EVENT, NAME_EVENT: NAME_EVENT, + TOPIC_EVENT: TOPIC_EVENT, + RESET_EVENT: RESET_EVENT, + + reset: function() { + reset(); + $rootScope.$broadcast(RESET_EVENT); + }, - handleEvent: function(event, isLiveEvent) { - // FIXME: event duplication suppression is all broken as the code currently expect to handles - // events multiple times to get their side-effects... -/* - if (eventMap[event.event_id]) { - console.log("discarding duplicate event: " + JSON.stringify(event)); - return; - } - else { - eventMap[event.event_id] = 1; + handleEvent: function(event, isLiveEvent, isStateEvent) { + + // 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); + + // Avoid duplicated events + // Needed for rooms where initialSync has not been done. + // In this case, we do not know where to start pagination. So, it starts from the END + // and we can have the same event (ex: joined, invitation) coming from the pagination + // AND from the event stream. + // FIXME: This workaround should be no more required when /initialSync on a particular room + // will be available (as opposite to the global /initialSync done at startup) + if (!isStateEvent) { // Do not consider state events + if (event.event_id && eventMap[event.event_id]) { + console.log("discarding duplicate event: " + JSON.stringify(event, undefined, 4)); + return; + } + else { + eventMap[event.event_id] = 1; + } } -*/ + if (event.type.indexOf('m.call.') === 0) { handleCallEvent(event, isLiveEvent); } @@ -189,7 +287,7 @@ angular.module('eventHandlerService', []) handleMessage(event, isLiveEvent); break; case "m.room.member": - handleRoomMember(event, isLiveEvent); + handleRoomMember(event, isLiveEvent, isStateEvent); break; case "m.presence": handlePresence(event, isLiveEvent); @@ -202,7 +300,10 @@ angular.module('eventHandlerService', []) handlePowerLevels(event, isLiveEvent); break; case 'm.room.name': - handleRoomName(event, isLiveEvent); + handleRoomName(event, isLiveEvent, isStateEvent); + break; + case 'm.room.topic': + handleRoomTopic(event, isLiveEvent, isStateEvent); break; default: console.log("Unable to handle event type " + event.type); @@ -214,20 +315,30 @@ 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) { + handleEvents: function(events, isLiveEvents, isStateEvents) { for (var i=0; i<events.length; i++) { - this.handleEvent(events[i], isLiveEvents); + this.handleEvent(events[i], isLiveEvents, isStateEvents); } }, - handleInitialSyncDone: function() { + // Handle messages from /initialSync or /messages + handleRoomMessages: function(room_id, messages, isLiveEvents) { + 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; + }, + + handleInitialSyncDone: function(initialSyncData) { console.log("# handleInitialSyncDone"); - InitialSyncDeferred.resolve($rootScope.events, $rootScope.presence); + initialSyncDeferred.resolve(initialSyncData); }, // Returns a promise that resolves when the initialSync request has been processed waitForInitialSyncCompletion: function() { - return InitialSyncDeferred.promise; + return initialSyncDeferred.promise; }, resetRoomMessages: function(room_id) { diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js index ed4f3b2ffc..03b805213d 100644 --- a/webclient/components/matrix/event-stream-service.js +++ b/webclient/components/matrix/event-stream-service.js @@ -104,15 +104,19 @@ angular.module('eventStreamService', []) settings.isActive = true; var deferred = $q.defer(); - // FIXME: We are discarding all the messages. - matrixService.rooms().then( + // Initial sync: get all information and the last message of all rooms of the user + matrixService.initialSync(1, false).then( function(response) { var rooms = response.data.rooms; for (var i = 0; i < rooms.length; ++i) { var room = rooms[i]; - // console.log("got room: " + room.room_id); + + if ("messages" in room) { + eventHandlerService.handleRoomMessages(room.room_id, room.messages, false); + } + if ("state" in room) { - eventHandlerService.handleEvents(room.state, false); + eventHandlerService.handleEvents(room.state, false, true); } } @@ -120,8 +124,9 @@ angular.module('eventStreamService', []) eventHandlerService.handleEvents(presence, false); // Initial sync is done - eventHandlerService.handleInitialSyncDone(); + eventHandlerService.handleInitialSyncDone(response); + // Start event streaming from that point settings.from = response.data.end; doEventStream(deferred); }, diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index 3cb5e8b693..2e3e2b0967 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -35,8 +35,13 @@ var forAllTracksOnStream = function(s, f) { forAllAudioTracksOnStream(s, f); } +navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; +window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible +window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; +window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; + angular.module('MatrixCall', []) -.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope) { +.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope, $timeout) { var MatrixCall = function(room_id) { this.room_id = room_id; this.call_id = "c" + new Date().getTime(); @@ -44,93 +49,117 @@ angular.module('MatrixCall', []) this.didConnect = false; } - navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - - window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection; + MatrixCall.prototype.createPeerConnection = function() { + var stunServer = 'stun:stun.l.google.com:19302'; + var pc; + if (window.mozRTCPeerConnection) { + pc = window.mozRTCPeerConnection({'url': stunServer}); + } else { + pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]}); + } + var self = this; + pc.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); }; + pc.onsignalingstatechange = function() { self.onSignallingStateChanged(); }; + pc.onicecandidate = function(c) { self.gotLocalIceCandidate(c); }; + pc.onaddstream = function(s) { self.onAddStream(s); }; + return pc; + } - MatrixCall.prototype.placeCall = function() { - self = this; + MatrixCall.prototype.placeCall = function(config) { + var self = this; matrixPhoneService.callPlaced(this); - navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); }); - self.state = 'wait_local_media'; + navigator.getUserMedia({audio: config.audio, video: config.video}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); }); + this.state = 'wait_local_media'; this.direction = 'outbound'; + this.config = config; }; MatrixCall.prototype.initWithInvite = function(msg) { this.msg = msg; - this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]}) - self= this; - this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); }; - this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); }; - this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); }; - this.peerConn.onaddstream = function(s) { self.onAddStream(s); }; - this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError); + this.peerConn = this.createPeerConnection(); + this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError); this.state = 'ringing'; this.direction = 'inbound'; }; MatrixCall.prototype.answer = function() { - console.trace("Answering call "+this.call_id); - self = this; - navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); }); - this.state = 'wait_local_media'; + console.log("Answering call "+this.call_id); + var self = this; + if (!this.localAVStream && !this.waitForLocalAVStream) { + navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); }); + this.state = 'wait_local_media'; + } else if (this.localAVStream) { + this.gotUserMediaForAnswer(this.localAVStream); + } else if (this.waitForLocalAVStream) { + this.state = 'wait_local_media'; + } }; MatrixCall.prototype.stopAllMedia = function() { if (this.localAVStream) { forAllTracksOnStream(this.localAVStream, function(t) { - t.stop(); + if (t.stop) t.stop(); }); } if (this.remoteAVStream) { forAllTracksOnStream(this.remoteAVStream, function(t) { - t.stop(); + if (t.stop) t.stop(); }); } }; - MatrixCall.prototype.hangup = function() { - console.trace("Ending call "+this.call_id); + MatrixCall.prototype.hangup = function(suppressEvent) { + console.log("Ending call "+this.call_id); this.stopAllMedia(); + if (this.peerConn) this.peerConn.close(); + + this.hangupParty = 'local'; var content = { version: 0, call_id: this.call_id, }; - matrixService.sendEvent(this.room_id, 'm.call.hangup', undefined, content).then(this.messageSent, this.messageSendFailed); + this.sendEventWithRetry('m.call.hangup', content); this.state = 'ended'; + if (this.onHangup && !suppressEvent) this.onHangup(this); }; MatrixCall.prototype.gotUserMediaForInvite = function(stream) { + if (this.successor) { + this.successor.gotUserMediaForAnswer(stream); + return; + } + if (this.state == 'ended') return; + this.localAVStream = stream; var audioTracks = stream.getAudioTracks(); for (var i = 0; i < audioTracks.length; i++) { audioTracks[i].enabled = true; } - this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]}) - self = this; - this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); }; - this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); }; - this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); }; - this.peerConn.onaddstream = function(s) { self.onAddStream(s); }; + this.peerConn = this.createPeerConnection(); this.peerConn.addStream(stream); + var self = this; this.peerConn.createOffer(function(d) { self.gotLocalOffer(d); }, function(e) { self.getLocalOfferFailed(e); }); - this.state = 'create_offer'; + $rootScope.$apply(function() { + self.state = 'create_offer'; + }); }; MatrixCall.prototype.gotUserMediaForAnswer = function(stream) { + if (this.state == 'ended') return; + this.localAVStream = stream; var audioTracks = stream.getAudioTracks(); for (var i = 0; i < audioTracks.length; i++) { audioTracks[i].enabled = true; } this.peerConn.addStream(stream); - self = this; + var self = this; var constraints = { 'mandatory': { 'OfferToReceiveAudio': true, @@ -138,23 +167,28 @@ angular.module('MatrixCall', []) }, }; this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints); - this.state = 'create_answer'; + // This can't be in an apply() because it's called by a predecessor call under glare conditions :( + self.state = 'create_answer'; }; MatrixCall.prototype.gotLocalIceCandidate = function(event) { - console.trace(event); + console.log(event); if (event.candidate) { var content = { version: 0, call_id: this.call_id, candidate: event.candidate }; - matrixService.sendEvent(this.room_id, 'm.call.candidate', undefined, content).then(this.messageSent, this.messageSendFailed); + this.sendEventWithRetry('m.call.candidate', content); } } MatrixCall.prototype.gotRemoteIceCandidate = function(cand) { - console.trace("Got ICE candidate from remote: "+cand); + console.log("Got ICE candidate from remote: "+cand); + if (this.state == 'ended') { + console.log("Ignoring remote ICE candidate because call has ended"); + return; + } var candidateObject = new RTCIceCandidate({ sdpMLineIndex: cand.label, candidate: cand.candidate @@ -163,12 +197,18 @@ angular.module('MatrixCall', []) }; MatrixCall.prototype.receivedAnswer = function(msg) { - this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError); + this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError); this.state = 'connecting'; }; MatrixCall.prototype.gotLocalOffer = function(description) { - console.trace("Created offer: "+description); + console.log("Created offer: "+description); + + if (this.state == 'ended') { + console.log("Ignoring newly created offer on call ID "+this.call_id+" because the call has ended"); + return; + } + this.peerConn.setLocalDescription(description); var content = { @@ -176,26 +216,27 @@ angular.module('MatrixCall', []) call_id: this.call_id, offer: description }; - matrixService.sendEvent(this.room_id, 'm.call.invite', undefined, content).then(this.messageSent, this.messageSendFailed); - this.state = 'invite_sent'; + this.sendEventWithRetry('m.call.invite', content); + + var self = this; + $rootScope.$apply(function() { + self.state = 'invite_sent'; + }); }; MatrixCall.prototype.createdAnswer = function(description) { - console.trace("Created answer: "+description); + console.log("Created answer: "+description); this.peerConn.setLocalDescription(description); var content = { version: 0, call_id: this.call_id, answer: description }; - matrixService.sendEvent(this.room_id, 'm.call.answer', undefined, content).then(this.messageSent, this.messageSendFailed); - this.state = 'connecting'; - }; - - MatrixCall.prototype.messageSent = function() { - }; - - MatrixCall.prototype.messageSendFailed = function(error) { + this.sendEventWithRetry('m.call.answer', content); + var self = this; + $rootScope.$apply(function() { + self.state = 'connecting'; + }); }; MatrixCall.prototype.getLocalOfferFailed = function(error) { @@ -204,33 +245,36 @@ angular.module('MatrixCall', []) MatrixCall.prototype.getUserMediaFailed = function() { this.onError("Couldn't start capturing audio! Is your microphone set up?"); + this.hangup(); }; MatrixCall.prototype.onIceConnectionStateChanged = function() { if (this.state == 'ended') return; // because ICE can still complete as we're ending the call - console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState); + console.log("Ice connection state changed to: "+this.peerConn.iceConnectionState); // ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') { - this.state = 'connected'; - this.didConnect = true; - $rootScope.$apply(); + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + self.didConnect = true; + }); } }; MatrixCall.prototype.onSignallingStateChanged = function() { - console.trace("Signalling state changed to: "+this.peerConn.signalingState); + console.log("call "+this.call_id+": Signalling state changed to: "+this.peerConn.signalingState); }; MatrixCall.prototype.onSetRemoteDescriptionSuccess = function() { - console.trace("Set remote description"); + console.log("Set remote description"); }; MatrixCall.prototype.onSetRemoteDescriptionError = function(e) { - console.trace("Failed to set remote description"+e); + console.log("Failed to set remote description"+e); }; MatrixCall.prototype.onAddStream = function(event) { - console.trace("Stream added"+event); + console.log("Stream added"+event); var s = event.stream; @@ -251,23 +295,79 @@ angular.module('MatrixCall', []) }; MatrixCall.prototype.onRemoteStreamStarted = function(event) { - this.state = 'connected'; + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + }); }; MatrixCall.prototype.onRemoteStreamEnded = function(event) { - this.state = 'ended'; - this.stopAllMedia(); - this.onHangup(); + console.log("Remote stream ended"); + var self = this; + $rootScope.$apply(function() { + self.state = 'ended'; + self.hangupParty = 'remote'; + self.stopAllMedia(); + if (self.peerConn.signalingState != 'closed') self.peerConn.close(); + if (self.onHangup) self.onHangup(self); + }); }; MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) { - this.state = 'connected'; + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + }); }; MatrixCall.prototype.onHangupReceived = function() { + console.log("Hangup received"); this.state = 'ended'; + this.hangupParty = 'remote'; this.stopAllMedia(); - this.onHangup(); + if (this.peerConn.signalingState != 'closed') this.peerConn.close(); + if (this.onHangup) this.onHangup(this); + }; + + MatrixCall.prototype.replacedBy = function(newCall) { + console.log(this.call_id+" being replaced by "+newCall.call_id); + if (this.state == 'wait_local_media') { + console.log("Telling new call to wait for local media"); + newCall.waitForLocalAVStream = true; + } else if (this.state == 'create_offer') { + console.log("Handing local stream to new call"); + newCall.localAVStream = this.localAVStream; + delete(this.localAVStream); + } else if (this.state == 'invite_sent') { + console.log("Handing local stream to new call"); + newCall.localAVStream = this.localAVStream; + delete(this.localAVStream); + } + this.successor = newCall; + this.hangup(true); + }; + + MatrixCall.prototype.sendEventWithRetry = function(evType, content) { + var ev = { type:evType, content:content, tries:1 }; + var self = this; + matrixService.sendEvent(this.room_id, evType, undefined, content).then(this.eventSent, function(error) { self.eventSendFailed(ev, error); } ); + }; + + MatrixCall.prototype.eventSent = function() { + }; + + MatrixCall.prototype.eventSendFailed = function(ev, error) { + if (ev.tries > 5) { + console.log("Failed to send event of type "+ev.type+" on attempt "+ev.tries+". Giving up."); + return; + } + var delayMs = 500 * Math.pow(2, ev.tries); + console.log("Failed to send event of type "+ev.type+". Retrying in "+delayMs+"ms"); + ++ev.tries; + var self = this; + $timeout(function() { + matrixService.sendEvent(self.room_id, ev.type, undefined, ev.content).then(self.eventSent, function(error) { self.eventSendFailed(ev, error); } ); + }, delayMs); }; return MatrixCall; diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js index 260e0827df..015a88bcad 100644 --- a/webclient/components/matrix/matrix-filter.js +++ b/webclient/components/matrix/matrix-filter.js @@ -31,15 +31,17 @@ angular.module('matrixFilter', []) } if (undefined === roomName) { - // Else, build the name from its users + 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) { diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js index ca86b473e7..b0dcf19100 100644 --- a/webclient/components/matrix/matrix-phone-service.js +++ b/webclient/components/matrix/matrix-phone-service.js @@ -22,6 +22,7 @@ angular.module('matrixPhoneService', []) }; matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT"; + matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT"; matrixPhoneService.allCalls = {}; matrixPhoneService.callPlaced = function(call) { @@ -38,29 +39,59 @@ angular.module('matrixPhoneService', []) call.call_id = msg.call_id; call.initWithInvite(msg); matrixPhoneService.allCalls[call.call_id] = call; - $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call); + + // Were we trying to call that user (room)? + var existingCall; + var callIds = Object.keys(matrixPhoneService.allCalls); + for (var i = 0; i < callIds.length; ++i) { + var thisCallId = callIds[i]; + var thisCall = matrixPhoneService.allCalls[thisCallId]; + + if (call.room_id == thisCall.room_id && thisCall.direction == 'outbound' + && (thisCall.state == 'wait_local_media' || thisCall.state == 'create_offer' || thisCall.state == 'invite_sent')) { + existingCall = thisCall; + break; + } + } + + if (existingCall) { + // If we've only got to wait_local_media or create_offer and we've got an invite, + // pick the incoming call because we know we haven't sent our invite yet + // otherwise, pick whichever call has the lowest call ID (by string comparison) + if (existingCall.state == 'wait_local_media' || existingCall.state == 'create_offer' || existingCall.call_id > call.call_id) { + console.log("Glare detected: answering incoming call "+call.call_id+" and canceling outgoing call "+existingCall.call_id); + existingCall.replacedBy(call); + call.answer(); + $rootScope.$broadcast(matrixPhoneService.REPLACED_CALL_EVENT, existingCall, call); + } else { + console.log("Glare detected: rejecting incoming call "+call.call_id+" and keeping outgoing call "+existingCall.call_id); + call.hangup(); + } + } else { + $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call); + } } else if (event.type == 'm.call.answer') { var call = matrixPhoneService.allCalls[msg.call_id]; if (!call) { - console.trace("Got answer for unknown call ID "+msg.call_id); + console.log("Got answer for unknown call ID "+msg.call_id); return; } call.receivedAnswer(msg); } else if (event.type == 'm.call.candidate') { var call = matrixPhoneService.allCalls[msg.call_id]; if (!call) { - console.trace("Got candidate for unknown call ID "+msg.call_id); + console.log("Got candidate for unknown call ID "+msg.call_id); return; } call.gotRemoteIceCandidate(msg.candidate); } else if (event.type == 'm.call.hangup') { var call = matrixPhoneService.allCalls[msg.call_id]; if (!call) { - console.trace("Got hangup for unknown call ID "+msg.call_id); + console.log("Got hangup for unknown call ID "+msg.call_id); return; } call.onHangupReceived(); - matrixPhoneService.allCalls[msg.call_id] = undefined; + delete(matrixPhoneService.allCalls[msg.call_id]); } }); diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 3c28c52fbe..68ef16800b 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -130,8 +130,9 @@ angular.module('matrixService', []) return doRequest("POST", path, undefined, req); }, - // List all rooms joined or been invited to - rooms: function(limit, feedback) { + // Get the user's current state: his presence, the list of his rooms with + // the last {limit} events + initialSync: function(limit, feedback) { // The REST path spec var path = "/initialSync"; @@ -234,6 +235,32 @@ angular.module('matrixService', []) return doRequest("GET", path, undefined, {}); }, + + setName: function(room_id, name) { + var data = { + name: name + }; + return this.sendStateEvent(room_id, "m.room.name", data); + }, + + setTopic: function(room_id, topic) { + var data = { + topic: topic + }; + return this.sendStateEvent(room_id, "m.room.topic", data); + }, + + + sendStateEvent: function(room_id, eventType, content, state_key) { + var path = "/rooms/$room_id/state/"+eventType; + if (state_key !== undefined) { + path += "/" + state_key; + } + room_id = encodeURIComponent(room_id); + path = path.replace("$room_id", room_id); + + return doRequest("PUT", path, undefined, content); + }, sendEvent: function(room_id, eventType, txn_id, content) { // The REST path spec |