summary refs log tree commit diff
path: root/webclient/components
diff options
context:
space:
mode:
Diffstat (limited to 'webclient/components')
-rw-r--r--webclient/components/matrix/event-handler-service.js186
-rw-r--r--webclient/components/matrix/event-stream-service.js6
-rw-r--r--webclient/components/matrix/matrix-call.js42
-rw-r--r--webclient/components/matrix/matrix-phone-service.js64
-rw-r--r--webclient/components/matrix/matrix-service.js163
5 files changed, 404 insertions, 57 deletions
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index 4604ff6192..258de9a31e 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";
@@ -44,6 +45,44 @@ angular.module('eventHandlerService', [])
     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;
 
@@ -63,13 +102,14 @@ angular.module('eventHandlerService', [])
     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
+                }
             };
         }
     };
@@ -136,6 +176,42 @@ 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 notification = new window.Notification(
+                        (displayname || event.user_id) +
+                        " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
+                    {
+                        "body": event.content.body,
+                        "icon": member ? member.avatar_url : undefined
+                    });
+                    $timeout(function() {
+                        notification.close();
+                    }, 5 * 1000);
+                }
+            }
         }
         else {
             $rootScope.events.rooms[event.room_id].messages.unshift(event);
@@ -257,7 +333,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. 
@@ -326,13 +404,29 @@ angular.module('eventHandlerService', [])
         },
 
         // 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++) {
+                    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--) {
+                    this.handleEvent(events[i], isLiveEvents, isLiveEvents);
+                }
+                // Store where to start pagination
+                $rootScope.events.rooms[room_id].pagination.earliest_token = messages.start;
+            }
         },
 
         handleInitialSyncDone: function(initialSyncData) {
@@ -347,6 +441,70 @@ 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;
         }
     };
 }]);
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 03b805213d..6f92332246 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) {
diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js
index fd21198d24..bf1e61ad7e 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/webclient/components/matrix/matrix-call.js
@@ -53,6 +53,8 @@ angular.module('MatrixCall', [])
         this.candidateSendTries = 0;
     }
 
+    MatrixCall.CALL_TIMEOUT = 60000;
+
     MatrixCall.prototype.createPeerConnection = function() {
         var stunServer = 'stun:stun.l.google.com:19302';
         var pc;
@@ -78,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() {
@@ -188,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';
     };
@@ -213,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';
         });
diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js
index 2d0732a8da..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);
@@ -79,21 +109,35 @@ angular.module('matrixPhoneService', [])
             call.receivedAnswer(msg);
         } else if (event.type == 'm.call.candidates') {
             var call = matrixPhoneService.allCalls[msg.call_id];
-            if (!call) {
+            if (!call && isLive) {
                 console.log("Got candidates for unknown call ID "+msg.call_id);
                 return;
-            }
-            for (var i = 0; i < msg.candidates.length; ++i) {
-                call.gotRemoteIceCandidate(msg.candidates[i]);
+            } 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]);
+                }
             }
         } 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