diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 6c3759878b..6338624486 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -130,6 +130,10 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
angular.element('#ringAudio')[0].pause();
angular.element('#ringbackAudio')[0].pause();
angular.element('#busyAudio')[0].play();
+ } else if (newVal == 'ended' && oldVal == 'invite_sent' && $rootScope.currentCall.hangupParty == 'local' && $rootScope.currentCall.hangupReason == 'invite_timeout') {
+ angular.element('#ringAudio')[0].pause();
+ angular.element('#ringbackAudio')[0].pause();
+ angular.element('#busyAudio')[0].play();
} else if (oldVal == 'invite_sent') {
angular.element('#ringbackAudio')[0].pause();
} else if (oldVal == 'ringing') {
diff --git a/webclient/app.css b/webclient/app.css
index 4a4ba7b8f4..704cd83947 100755
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -538,6 +538,10 @@ a:active { color: #000; }
color: #F00;
}
+.messageBing {
+ color: #00F;
+}
+
#room-fullscreen-image {
position: absolute;
top: 0px;
@@ -599,7 +603,7 @@ a:active { color: #000; }
width: auto;
}
-.recentsRoomSummaryTS {
+.recentsRoomSummaryUsersCount, .recentsRoomSummaryTS {
color: #888;
font-size: 12px;
width: 7em;
@@ -612,6 +616,11 @@ a:active { color: #000; }
padding-bottom: 5px;
}
+/* Do not show users count in the recents fragment displayed on the room page */
+#roomPage .recentsRoomSummaryUsersCount {
+ width: 0em;
+}
+
/*** Recents in the room page ***/
#roomRecentsTableWrapper {
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
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
index c0c4ea11aa..8ba817ca68 100644
--- a/webclient/home/home-controller.js
+++ b/webclient/home/home-controller.js
@@ -117,6 +117,10 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
matrixService.getDisplayName($scope.config.user_id).then(
function(response) {
$scope.profile.displayName = response.data.displayname;
+ var config = matrixService.config();
+ config.display_name = response.data.displayname;
+ matrixService.setConfig(config);
+ matrixService.saveConfig();
},
function(error) {
$scope.feedback = "Can't load display name";
diff --git a/webclient/index.html b/webclient/index.html
index 9eea08215c..7e4dcb8345 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -62,7 +62,8 @@
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
<span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'remote'">Call Rejected</span>
- <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">Call Canceled</span>
+ <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local' && currentCall.hangupReason == undefined">Call Canceled</span>
+ <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local' && currentCall.hangupReason == 'invite_timeout'">User Not Responding</span>
<span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'outbound'">Call Ended</span>
<span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'inbound'">Call Canceled</span>
<span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'inbound'">Call Ended</span>
diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js
index a0db0538f3..ee8a41c366 100644
--- a/webclient/recents/recents-controller.js
+++ b/webclient/recents/recents-controller.js
@@ -16,134 +16,16 @@
'use strict';
-angular.module('RecentsController', ['matrixService', 'matrixFilter', 'eventHandlerService'])
-.controller('RecentsController', ['$rootScope', '$scope', 'matrixService', 'eventHandlerService',
- function($rootScope, $scope, matrixService, eventHandlerService) {
-
- // FIXME: Angularjs reloads the controller (and resets its $scope) each time
- // the page URL changes, use $rootScope to avoid to have to reload data
- $rootScope.rooms;
+angular.module('RecentsController', ['matrixService', 'matrixFilter'])
+.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService',
+ function($rootScope, $scope, eventHandlerService) {
+
+ // Expose the service to the view
+ $scope.eventHandlerService = eventHandlerService;
// $rootScope of the parent where the recents component is included can override this value
// in order to highlight a specific room in the list
$rootScope.recentsSelectedRoomID;
-
- var listenToEventStream = function() {
- // Refresh the list on matrix invitation and message event
- $rootScope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
- if (isLive) {
- if (!$rootScope.rooms[event.room_id]) {
- // The user has joined a new room, which we do not have data yet. The reason is that
- // the room has appeared in the scope of the user rooms after the global initialSync
- // FIXME: an initialSync on this specific room should be done
- $rootScope.rooms[event.room_id] = {
- room_id:event.room_id
- };
- }
- else if (event.state_key === matrixService.config().user_id && "invite" !== event.membership && "join" !== event.membership) {
- // The user has been kicked or banned from the room, remove this room from the recents
- delete $rootScope.rooms[event.room_id];
- }
-
- if ($rootScope.rooms[event.room_id]) {
- $rootScope.rooms[event.room_id].lastMsg = event;
- }
-
- // Update room users count
- $rootScope.rooms[event.room_id].numUsersInRoom = getUsersCountInRoom(event.room_id);
- }
- });
- $rootScope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
- if (isLive) {
- $rootScope.rooms[event.room_id].lastMsg = event;
- }
- });
- $rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
- if (isLive) {
- $rootScope.rooms[event.room_id].lastMsg = event;
- }
- });
- $rootScope.$on(eventHandlerService.ROOM_CREATE_EVENT, function(ngEvent, event, isLive) {
- if (isLive) {
- $rootScope.rooms[event.room_id] = event;
- }
- });
- $rootScope.$on(eventHandlerService.NAME_EVENT, function(ngEvent, event, isLive) {
- if (isLive) {
- $rootScope.rooms[event.room_id].lastMsg = event;
- }
- });
- $rootScope.$on(eventHandlerService.TOPIC_EVENT, function(ngEvent, event, isLive) {
- if (isLive) {
- $rootScope.rooms[event.room_id].lastMsg = event;
- }
- });
- };
-
- /**
- * 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
- */
- var 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;
- };
-
- $scope.onInit = function() {
- // Init recents list only once
- if ($rootScope.rooms) {
- return;
- }
-
- $rootScope.rooms = {};
-
- // Use initialSync data to init the recents list
- eventHandlerService.waitForInitialSyncCompletion().then(
- function(initialSyncData) {
-
- var rooms = initialSyncData.data.rooms;
- for (var i=0; i<rooms.length; i++) {
- var room = rooms[i];
-
- // Add room_alias & room_display_name members
- $rootScope.rooms[room.room_id] = angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
-
- // Create a shortcut for the last message of this room
- if (room.messages && room.messages.chunk && room.messages.chunk[0]) {
- $rootScope.rooms[room.room_id].lastMsg = room.messages.chunk[0];
- }
-
- $rootScope.rooms[room.room_id].numUsersInRoom = getUsersCountInRoom(room.room_id);
- }
-
- // From now, update recents from the stream
- listenToEventStream();
- },
- function(error) {
- $rootScope.feedback = "Failure: " + error.data;
- }
- );
- };
-
- // Clean data when user logs out
- $scope.$on(eventHandlerService.RESET_EVENT, function() {
- delete $rootScope.rooms;
- });
}]);
diff --git a/webclient/recents/recents-filter.js b/webclient/recents/recents-filter.js
index d80de6fbeb..2fd4dbe98b 100644
--- a/webclient/recents/recents-filter.js
+++ b/webclient/recents/recents-filter.js
@@ -17,31 +17,48 @@
'use strict';
angular.module('RecentsController')
-.filter('orderRecents', function() {
+.filter('orderRecents', ["matrixService", "eventHandlerService", function(matrixService, eventHandlerService) {
return function(rooms) {
+ var user_id = matrixService.config().user_id;
+
// Transform the dict into an array
// The key, room_id, is already in value objects
var filtered = [];
- angular.forEach(rooms, function(value, key) {
- filtered.push( value );
+ angular.forEach(rooms, function(room, room_id) {
+
+ // Show the room only if the user has joined it or has been invited
+ // (ie, do not show it if he has been banned)
+ var member = eventHandlerService.getMember(room_id, user_id);
+ if (member && ("invite" === member.membership || "join" === member.membership)) {
+
+ // Count users here
+ // TODO: Compute it directly in eventHandlerService
+ room.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id);
+
+ filtered.push(room);
+ }
});
// And time sort them
// The room with the lastest message at first
- filtered.sort(function (a, b) {
+ filtered.sort(function (roomA, roomB) {
+
+ var lastMsgRoomA = eventHandlerService.getLastMessage(roomA.room_id, true);
+ var lastMsgRoomB = eventHandlerService.getLastMessage(roomB.room_id, true);
+
// Invite message does not have a body message nor ts
// Puth them at the top of the list
- if (undefined === a.lastMsg) {
+ if (undefined === lastMsgRoomA) {
return -1;
}
- else if (undefined === b.lastMsg) {
+ else if (undefined === lastMsgRoomB) {
return 1;
}
else {
- return b.lastMsg.ts - a.lastMsg.ts;
+ return lastMsgRoomB.ts - lastMsgRoomA.ts;
}
});
return filtered;
};
-});
\ No newline at end of file
+}]);
\ No newline at end of file
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index ca7636e36a..e783d3a6b4 100644
--- a/webclient/recents/recents.html
+++ b/webclient/recents/recents.html
@@ -1,20 +1,24 @@
-<div ng-controller="RecentsController" data-ng-init="onInit()">
+<div ng-controller="RecentsController">
<table class="recentsTable">
- <tbody ng-repeat="(rm_id, room) in rooms | orderRecents"
+ <tbody ng-repeat="(index, room) in events.rooms | orderRecents"
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
class ="recentsRoom"
- ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
+ ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
<tr>
<td class="recentsRoomName">
{{ room.room_id | mRoomName }}
</td>
- <td class="recentsRoomSummaryTS">
+ <td class="recentsRoomSummaryUsersCount">
<span ng-show="undefined !== room.numUsersInRoom">
{{ room.numUsersInRoom || '1' }} {{ room.numUsersInRoom == 1 ? 'user' : 'users' }}
</span>
</td>
<td class="recentsRoomSummaryTS">
- {{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
+ <!-- Use a temp var as alias to the last room message.
+ Declaring it in this way ensures the data-binding -->
+ {{ lastMsg = eventHandlerService.getLastMessage(room.room_id, true);"" }}
+
+ {{ (lastMsg.ts) | date:'MMM d HH:mm' }}
</td>
</tr>
@@ -25,67 +29,67 @@
{{ room.inviter | mUserDisplayName: room.room_id }} invited you
</div>
- <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type">
+ <div ng-hide="room.membership === 'invite'" ng-switch="lastMsg.type">
<div ng-switch-when="m.room.member">
- <span ng-if="'join' === room.lastMsg.content.membership">
- {{ room.lastMsg.state_key | mUserDisplayName: room.room_id}} joined
+ <span ng-if="'join' === lastMsg.content.membership">
+ {{ lastMsg.state_key | mUserDisplayName: room.room_id}} joined
</span>
- <span ng-if="'leave' === room.lastMsg.content.membership">
- <span ng-if="room.lastMsg.user_id === room.lastMsg.state_key">
- {{room.lastMsg.state_key | mUserDisplayName: room.room_id }} left
+ <span ng-if="'leave' === lastMsg.content.membership">
+ <span ng-if="lastMsg.user_id === lastMsg.state_key">
+ {{lastMsg.state_key | mUserDisplayName: room.room_id }} left
</span>
- <span ng-if="room.lastMsg.user_id !== room.lastMsg.state_key">
- {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
- {{ {"join": "kicked", "ban": "unbanned"}[room.lastMsg.content.prev] }}
- {{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
+ <span ng-if="lastMsg.user_id !== lastMsg.state_key">
+ {{ lastMsg.user_id | mUserDisplayName: room.room_id }}
+ {{ {"join": "kicked", "ban": "unbanned"}[lastMsg.content.prev] }}
+ {{ lastMsg.state_key | mUserDisplayName: room.room_id }}
</span>
- <span ng-if="'join' === room.lastMsg.content.prev && room.lastMsg.content.reason">
- : {{ room.lastMsg.content.reason }}
+ <span ng-if="'join' === lastMsg.content.prev && lastMsg.content.reason">
+ : {{ lastMsg.content.reason }}
</span>
</span>
- <span ng-if="'invite' === room.lastMsg.content.membership || 'ban' === room.lastMsg.content.membership">
- {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
- {{ {"invite": "invited", "ban": "banned"}[room.lastMsg.content.membership] }}
- {{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
- <span ng-if="'ban' === room.lastMsg.content.prev && room.lastMsg.content.reason">
- : {{ room.lastMsg.content.reason }}
+ <span ng-if="'invite' === lastMsg.content.membership || 'ban' === lastMsg.content.membership">
+ {{ lastMsg.user_id | mUserDisplayName: room.room_id }}
+ {{ {"invite": "invited", "ban": "banned"}[lastMsg.content.membership] }}
+ {{ lastMsg.state_key | mUserDisplayName: room.room_id }}
+ <span ng-if="'ban' === lastMsg.content.prev && lastMsg.content.reason">
+ : {{ lastMsg.content.reason }}
</span>
</span>
</div>
<div ng-switch-when="m.room.message">
- <div ng-switch="room.lastMsg.content.msgtype">
+ <div ng-switch="lastMsg.content.msgtype">
<div ng-switch-when="m.text">
- {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} :
- <span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'">
+ {{ lastMsg.user_id | mUserDisplayName: room.room_id }} :
+ <span ng-bind-html="(lastMsg.content.body) | linky:'_blank'">
</span>
</div>
<div ng-switch-when="m.image">
- {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} sent an image
+ {{ lastMsg.user_id | mUserDisplayName: room.room_id }} sent an image
</div>
<div ng-switch-when="m.emote">
- <span ng-bind-html="'* ' + (room.lastMsg.user_id | mUserDisplayName: room.room_id) + ' ' + room.lastMsg.content.body | linky:'_blank'">
+ <span ng-bind-html="'* ' + (lastMsg.user_id | mUserDisplayName: room.room_id) + ' ' + lastMsg.content.body | linky:'_blank'">
</span>
</div>
<div ng-switch-default>
- {{ room.lastMsg.content }}
+ {{ lastMsg.content }}
</div>
</div>
</div>
<div ng-switch-when="m.room.topic">
- {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} changed the topic to: {{ room.lastMsg.content.topic }}
+ {{ lastMsg.user_id | mUserDisplayName: room.room_id }} changed the topic to: {{ lastMsg.content.topic }}
</div>
<div ng-switch-when="m.room.name">
- {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} changed the room name to: {{ room.lastMsg.content.name }}
+ {{ lastMsg.user_id | mUserDisplayName: room.room_id }} changed the room name to: {{ lastMsg.content.name }}
</div>
<div ng-switch-default>
- <div ng-if="room.lastMsg.type.indexOf('m.call.') === 0">
+ <div ng-if="lastMsg.type.indexOf('m.call.') === 0">
Call
</div>
</div>
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 50d902ae47..6e1d83a23d 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -15,8 +15,8 @@ limitations under the License.
*/
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
-.controller('RoomController', ['$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall',
- function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) {
+.controller('RoomController', ['$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
+ function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
'use strict';
var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320;
@@ -139,27 +139,11 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
if (isLive && event.room_id === $scope.room_id) {
scrollToBottom();
-
- if (window.Notification) {
- // Show notification when the window is hidden, or the user is idle
- if (document.hidden || matrixService.presence.unavailable === mPresence.getState()) {
- var notification = new window.Notification(
- ($scope.members[event.user_id].displayname || event.user_id) +
- " (" + ($scope.room_alias || $scope.room_id) + ")", // FIXME: don't leak room_ids here
- {
- "body": event.content.body,
- "icon": $scope.members[event.user_id].avatar_url
- });
- $timeout(function() {
- notification.close();
- }, 5 * 1000);
- }
- }
}
});
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
- if (isLive) {
+ if (isLive && event.room_id === $scope.room_id) {
if ($scope.state.waiting_for_joined_event) {
// The user has successfully joined the room, we can getting data for this room
$scope.state.waiting_for_joined_event = false;
@@ -177,19 +161,33 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
else {
user = event.user_id;
}
-
if ("ban" === event.membership) {
$scope.state.permission_denied = "You have been banned by " + user;
}
else {
$scope.state.permission_denied = "You have been kicked by " + user;
- }
-
+ }
}
else {
scrollToBottom();
updateMemberList(event);
+
+ // Notify when a user joins
+ if ((document.hidden || matrixService.presence.unavailable === mPresence.getState())
+ && event.state_key !== $scope.state.user_id && "join" === event.membership) {
+ debugger;
+ var notification = new window.Notification(
+ event.content.displayname +
+ " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
+ {
+ "body": event.content.displayname + " joined",
+ "icon": event.content.avatar_url ? event.content.avatar_url : undefined
+ });
+ $timeout(function() {
+ notification.close();
+ }, 5 * 1000);
+ }
}
}
});
@@ -235,7 +233,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
matrixService.paginateBackMessages($scope.room_id, $rootScope.events.rooms[$scope.room_id].pagination.earliest_token, numItems).then(
function(response) {
- eventHandlerService.handleRoomMessages($scope.room_id, response.data, false);
+ eventHandlerService.handleRoomMessages($scope.room_id, response.data, false, 'b');
if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
// no more messages to paginate. this currently never gets turned true again, as we never
// expire paginated contents in the current implementation.
@@ -676,6 +674,10 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
var onInit2 = function() {
console.log("onInit2");
+ // Scroll down as soon as possible so that we point to the last message
+ // if it already exists in memory
+ scrollToBottom(true);
+
// Make sure the initialSync has been before going further
eventHandlerService.waitForInitialSyncCompletion().then(
function() {
@@ -684,6 +686,10 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
// The room members is available in the data fetched by initialSync
if ($rootScope.events.rooms[$scope.room_id]) {
+
+ // There is no need to do a 1st pagination (initialSync provided enough to fill a page)
+ $scope.state.first_pagination = false;
+
var members = $rootScope.events.rooms[$scope.room_id].members;
// Update the member list
@@ -743,9 +749,18 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
// Arm list timing update timer
updateMemberListPresenceAge();
- // Start pagination
+ // Allow pagination
$scope.state.can_paginate = true;
- paginate(MESSAGES_PER_PAGINATION);
+
+ // Do a first pagination only if it is required
+ // FIXME: Should be no more require when initialSync/{room_id} will be available
+ if ($scope.state.first_pagination) {
+ paginate(MESSAGES_PER_PAGINATION);
+ }
+ else {
+ // There are already messages, go to the last message
+ scrollToBottom(true);
+ }
},
function(error) {
$scope.feedback = "Failed get member list: " + error.data.error;
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 9d617eadd8..886c2afe64 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -105,7 +105,7 @@
<span ng-show='msg.content.msgtype === "m.text"'
class="message"
- ng-class="msg.echo_msg_state"
+ ng-class="containsBingWord(msg.content.body) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
<span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call</span>
diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js
index 8c877a24e9..9cdace704a 100644
--- a/webclient/settings/settings-controller.js
+++ b/webclient/settings/settings-controller.js
@@ -194,7 +194,16 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
/*** Desktop notifications section ***/
$scope.settings = {
- notifications: undefined
+ notifications: undefined,
+ bingWords: matrixService.config().bingWords
+ };
+
+ $scope.saveBingWords = function() {
+ console.log("Saving words: "+JSON.stringify($scope.settings.bingWords));
+ var config = matrixService.config();
+ config.bingWords = $scope.settings.bingWords;
+ matrixService.setConfig(config);
+ matrixService.saveConfig();
};
// If the browser supports it, check the desktop notification state
diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html
index c358a6e9d8..0a5a3db51f 100644
--- a/webclient/settings/settings.html
+++ b/webclient/settings/settings.html
@@ -51,7 +51,16 @@
<h3>Desktop notifications</h3>
<div class="section" ng-switch="settings.notifications">
<div ng-switch-when="granted">
- Notifications are enabled.
+ Notifications are enabled. You will be alerted when a message contains your user ID or display name.
+ <div class="section">
+ <h4>Additional words to alert on:</h4>
+ <p>Leave blank to alert on all messages.</p>
+ <input size=40 name="bingWords" ng-model="settings.bingWords" ng-list placeholder="Enter words separated with , (supports regex)"
+ ng-blur="saveBingWords()"/>
+ <ul>
+ <li ng-repeat="word in settings.bingWords">{{word}}</li>
+ </ul>
+ </div>
</div>
<div ng-switch-when="denied">
You have denied permission for notifications.<br/>
|