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 064f626f0b..736aea660c 100755
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -528,9 +528,8 @@ a:active { color: #000; }
}
.bubble .message {
- /* Break lines when encountering CR+LF */
- /* FIXME: this breaks wordwrapping. We need to s#CRLF#<br/>#g instead */
-/* white-space: pre; */
+ /* Wrap words and break lines on CR+LF */
+ white-space: pre-wrap;
}
.bubble .messagePending {
opacity: 0.3
@@ -539,6 +538,10 @@ a:active { color: #000; }
color: #F00;
}
+.messageBing {
+ color: #00F;
+}
+
#room-fullscreen-image {
position: absolute;
top: 0px;
@@ -600,7 +603,11 @@ a:active { color: #000; }
width: auto;
}
-.recentsRoomSummaryTS {
+.recentsPublicRoom {
+ font-weight: bold;
+}
+
+.recentsRoomSummaryUsersCount, .recentsRoomSummaryTS {
color: #888;
font-size: 12px;
width: 7em;
@@ -613,6 +620,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 705a5a07f2..ad69d297fa 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -27,7 +27,8 @@ Typically, this service will store events or broadcast them to any listeners
if typically all the $on method would do is update its own $scope.
*/
angular.module('eventHandlerService', [])
-.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
+.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence',
+function(matrixService, $rootScope, $q, $timeout, mPresence) {
var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
var MSG_EVENT = "MSG_EVENT";
var MEMBER_EVENT = "MEMBER_EVENT";
@@ -38,6 +39,51 @@ angular.module('eventHandlerService', [])
var TOPIC_EVENT = "TOPIC_EVENT";
var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted
+ // used for dedupping events - could be expanded in future...
+ // FIXME: means that we leak memory over time (along with lots of the rest
+ // of the app, given we never try to reap memory yet)
+ var eventMap = {};
+
+ $rootScope.presence = {};
+
+ // TODO: This is attached to the rootScope so .html can just go containsBingWord
+ // for determining classes so it is easy to highlight bing messages. It seems a
+ // bit strange to put the impl in this service though, but I can't think of a better
+ // file to put it in.
+ $rootScope.containsBingWord = function(content) {
+ if (!content || $.type(content) != "string") {
+ return false;
+ }
+ var bingWords = matrixService.config().bingWords;
+ var shouldBing = false;
+
+ // case-insensitive name check for user_id OR display_name if they exist
+ var myUserId = matrixService.config().user_id;
+ if (myUserId) {
+ myUserId = myUserId.toLocaleLowerCase();
+ }
+ var myDisplayName = matrixService.config().display_name;
+ if (myDisplayName) {
+ myDisplayName = myDisplayName.toLocaleLowerCase();
+ }
+ if ( (myDisplayName && content.toLocaleLowerCase().indexOf(myDisplayName) != -1) ||
+ (myUserId && content.toLocaleLowerCase().indexOf(myUserId) != -1) ) {
+ shouldBing = true;
+ }
+
+ // bing word list check
+ if (bingWords && !shouldBing) {
+ for (var i=0; i<bingWords.length; i++) {
+ var re = RegExp(bingWords[i]);
+ if (content.search(re) != -1) {
+ shouldBing = true;
+ break;
+ }
+ }
+ }
+ return shouldBing;
+ };
+
var initialSyncDeferred;
var reset = function() {
@@ -46,26 +92,24 @@ angular.module('eventHandlerService', [])
$rootScope.events = {
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
};
- }
- reset();
- // used for dedupping events - could be expanded in future...
- // FIXME: means that we leak memory over time (along with lots of the rest
- // of the app, given we never try to reap memory yet)
- var eventMap = {};
+ $rootScope.presence = {};
+
+ eventMap = {};
+ };
+ reset();
- $rootScope.presence = {};
-
var initRoom = function(room_id) {
if (!(room_id in $rootScope.events.rooms)) {
console.log("Creating new handler entry for " + room_id);
- $rootScope.events.rooms[room_id] = {};
- $rootScope.events.rooms[room_id].messages = [];
- $rootScope.events.rooms[room_id].members = {};
-
- // Pagination information
- $rootScope.events.rooms[room_id].pagination = {
- earliest_token: "END" // how far back we've paginated
+ $rootScope.events.rooms[room_id] = {
+ room_id: room_id,
+ messages: [],
+ members: {},
+ // Pagination information
+ pagination: {
+ earliest_token: "END" // how far back we've paginated
+ }
};
}
};
@@ -132,6 +176,48 @@ angular.module('eventHandlerService', [])
else {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
+
+ if (window.Notification && event.user_id != matrixService.config().user_id) {
+ var shouldBing = $rootScope.containsBingWord(event.content.body);
+
+ // TODO: Binging every message when idle doesn't make much sense. Can we use this more sensibly?
+ // Unfortunately document.hidden = false on ubuntu chrome if chrome is minimised / does not have focus;
+ // true when you swap tabs though. However, for the case where the chat screen is OPEN and there is
+ // another window on top, we want to be notifying for those events. This DOES mean that there will be
+ // notifications when currently viewing the chat screen though, but that is preferable to the alternative imo.
+ var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState());
+
+ // always bing if there are 0 bing words... apparently.
+ var bingWords = matrixService.config().bingWords;
+ if (bingWords && bingWords.length === 0) {
+ shouldBing = true;
+ }
+
+ if (shouldBing) {
+ console.log("Displaying notification for "+JSON.stringify(event));
+ var member = $rootScope.events.rooms[event.room_id].members[event.user_id];
+ var displayname = undefined;
+ if (member) {
+ displayname = member.displayname;
+ }
+
+ var message = event.content.body;
+ if (event.content.msgtype === "m.emote") {
+ message = "* " + displayname + " " + message;
+ }
+
+ var notification = new window.Notification(
+ (displayname || event.user_id) +
+ " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
+ {
+ "body": message,
+ "icon": member ? member.avatar_url : undefined
+ });
+ $timeout(function() {
+ notification.close();
+ }, 5 * 1000);
+ }
+ }
}
else {
$rootScope.events.rooms[event.room_id].messages.unshift(event);
@@ -157,8 +243,9 @@ angular.module('eventHandlerService', [])
// add membership changes as if they were a room message if something interesting changed
// Exception: Do not do this if the event is a room state event because such events already come
// as room messages events. Moreover, when they come as room messages events, they are relatively ordered
- // with other other room messages
- if (event.content.prev !== event.content.membership && !isStateEvent) {
+ // with other other room messages XXX This is no longer true, you only get a single event, not a room message event.
+ // FIXME: This possibly reintroduces multiple join messages.
+ if (event.content.prev !== event.content.membership) { // && !isStateEvent
if (isLiveEvent) {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
@@ -204,7 +291,7 @@ angular.module('eventHandlerService', [])
var handleCallEvent = function(event, isLiveEvent) {
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
- if (event.type == 'm.call.invite') {
+ if (event.type === 'm.call.invite') {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
};
@@ -231,7 +318,7 @@ angular.module('eventHandlerService', [])
}
}
return index;
- }
+ };
return {
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
@@ -253,7 +340,9 @@ angular.module('eventHandlerService', [])
// FIXME: /initialSync on a particular room is not yet available
// So initRoom on a new room is not called. Make sure the room data is initialised here
- initRoom(event.room_id);
+ if (event.room_id) {
+ initRoom(event.room_id);
+ }
// Avoid duplicated events
// Needed for rooms where initialSync has not been done.
@@ -287,6 +376,7 @@ angular.module('eventHandlerService', [])
handleMessage(event, isLiveEvent);
break;
case "m.room.member":
+ isStateEvent = true;
handleRoomMember(event, isLiveEvent, isStateEvent);
break;
case "m.presence":
@@ -316,19 +406,39 @@ angular.module('eventHandlerService', [])
// isLiveEvents determines whether notifications should be shown, whether
// messages get appended to the start/end of lists, etc.
handleEvents: function(events, isLiveEvents, isStateEvents) {
+ // XXX FIXME TODO: isStateEvents is being left as undefined sometimes. It makes no sense
+ // to have isStateEvents as an arg, since things like m.room.member are ALWAYS state events.
for (var i=0; i<events.length; i++) {
this.handleEvent(events[i], isLiveEvents, isStateEvents);
}
},
// Handle messages from /initialSync or /messages
- handleRoomMessages: function(room_id, messages, isLiveEvents) {
+ handleRoomMessages: function(room_id, messages, isLiveEvents, dir) {
initRoom(room_id);
- this.handleEvents(messages.chunk, isLiveEvents);
- // Store how far back we've paginated
- // This assumes the paginations requests are contiguous and in reverse chronological order
- $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
+ var events = messages.chunk;
+
+ // Handles messages according to their time order
+ if (dir && 'b' === dir) {
+ // paginateBackMessages requests messages to be in reverse chronological order
+ for (var i=0; i<events.length; i++) {
+ // FIXME: Being live != being state
+ this.handleEvent(events[i], isLiveEvents, isLiveEvents);
+ }
+
+ // Store how far back we've paginated
+ $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
+ }
+ else {
+ // InitialSync returns messages in chronological order
+ for (var i=events.length - 1; i>=0; i--) {
+ // FIXME: Being live != being state
+ this.handleEvent(events[i], isLiveEvents, isLiveEvents);
+ }
+ // Store where to start pagination
+ $rootScope.events.rooms[room_id].pagination.earliest_token = messages.start;
+ }
},
handleInitialSyncDone: function(initialSyncData) {
@@ -343,6 +453,82 @@ angular.module('eventHandlerService', [])
resetRoomMessages: function(room_id) {
resetRoomMessages(room_id);
+ },
+
+ /**
+ * Return the last message event of a room
+ * @param {String} room_id the room id
+ * @param {Boolean} filterFake true to not take into account fake messages
+ * @returns {undefined | Event} the last message event if available
+ */
+ getLastMessage: function(room_id, filterEcho) {
+ var lastMessage;
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ for (var i = room.messages.length - 1; i >= 0; i--) {
+ var message = room.messages[i];
+
+ if (!filterEcho || undefined === message.echo_msg_state) {
+ lastMessage = message;
+ break;
+ }
+ }
+ }
+
+ return lastMessage;
+ },
+
+ /**
+ * Compute the room users number, ie the number of members who has joined the room.
+ * @param {String} room_id the room id
+ * @returns {undefined | Number} the room users number if available
+ */
+ getUsersCountInRoom: function(room_id) {
+ var memberCount;
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ memberCount = 0;
+
+ for (var i in room.members) {
+ var member = room.members[i];
+
+ if ("join" === member.membership) {
+ memberCount = memberCount + 1;
+ }
+ }
+ }
+
+ return memberCount;
+ },
+
+ /**
+ * Get the member object of a room member
+ * @param {String} room_id the room id
+ * @param {String} user_id the id of the user
+ * @returns {undefined | Object} the member object of this user in this room if he is part of the room
+ */
+ getMember: function(room_id, user_id) {
+ var member;
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ member = room.members[user_id];
+ }
+ return member;
+ },
+
+ setRoomVisibility: function(room_id, visible) {
+ if (!visible) {
+ return;
+ }
+ initRoom(room_id);
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ room.visibility = visible;
+ }
}
};
}]);
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 03b805213d..5af1ab2911 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -104,8 +104,10 @@ angular.module('eventStreamService', [])
settings.isActive = true;
var deferred = $q.defer();
- // Initial sync: get all information and the last message of all rooms of the user
- matrixService.initialSync(1, false).then(
+ // Initial sync: get all information and the last 30 messages of all rooms of the user
+ // 30 messages should be enough to display a full page of messages in a room
+ // without requiring to make an additional request
+ matrixService.initialSync(30, false).then(
function(response) {
var rooms = response.data.rooms;
for (var i = 0; i < rooms.length; ++i) {
@@ -118,6 +120,8 @@ angular.module('eventStreamService', [])
if ("state" in room) {
eventHandlerService.handleEvents(room.state, false, true);
}
+
+ eventHandlerService.setRoomVisibility(room.room_id, room.visibility);
}
var presence = response.data.presence;
diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js
index 2e3e2b0967..2ecb8b05ff 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/webclient/components/matrix/matrix-call.js
@@ -47,13 +47,19 @@ angular.module('MatrixCall', [])
this.call_id = "c" + new Date().getTime();
this.state = 'fledgling';
this.didConnect = false;
+
+ // a queue for candidates waiting to go out. We try to amalgamate candidates into a single candidate message where possible
+ this.candidateSendQueue = [];
+ this.candidateSendTries = 0;
}
+ MatrixCall.CALL_TIMEOUT = 60000;
+
MatrixCall.prototype.createPeerConnection = function() {
var stunServer = 'stun:stun.l.google.com:19302';
var pc;
if (window.mozRTCPeerConnection) {
- pc = window.mozRTCPeerConnection({'url': stunServer});
+ pc = new window.mozRTCPeerConnection({'url': stunServer});
} else {
pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]});
}
@@ -74,12 +80,30 @@ angular.module('MatrixCall', [])
this.config = config;
};
- MatrixCall.prototype.initWithInvite = function(msg) {
- this.msg = msg;
+ MatrixCall.prototype.initWithInvite = function(event) {
+ this.msg = event.content;
this.peerConn = this.createPeerConnection();
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
this.state = 'ringing';
this.direction = 'inbound';
+ var self = this;
+ $timeout(function() {
+ if (self.state == 'ringing') {
+ self.state = 'ended';
+ self.hangupParty = 'remote'; // effectively
+ self.stopAllMedia();
+ if (self.peerConn.signalingState != 'closed') self.peerConn.close();
+ if (self.onHangup) self.onHangup(self);
+ }
+ }, this.msg.lifetime - event.age);
+ };
+
+ // perverse as it may seem, sometimes we want to instantiate a call with a hangup message
+ // (because when getting the state of the room on load, events come in reverse order and
+ // we want to remember that a call has been hung up)
+ MatrixCall.prototype.initWithHangup = function(event) {
+ this.msg = event.content;
+ this.state = 'ended';
};
MatrixCall.prototype.answer = function() {
@@ -174,12 +198,7 @@ angular.module('MatrixCall', [])
MatrixCall.prototype.gotLocalIceCandidate = function(event) {
console.log(event);
if (event.candidate) {
- var content = {
- version: 0,
- call_id: this.call_id,
- candidate: event.candidate
- };
- this.sendEventWithRetry('m.call.candidate', content);
+ this.sendCandidate(event.candidate);
}
}
@@ -189,14 +208,12 @@ angular.module('MatrixCall', [])
console.log("Ignoring remote ICE candidate because call has ended");
return;
}
- var candidateObject = new RTCIceCandidate({
- sdpMLineIndex: cand.label,
- candidate: cand.candidate
- });
- this.peerConn.addIceCandidate(candidateObject, function() {}, function(e) {});
+ this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {});
};
MatrixCall.prototype.receivedAnswer = function(msg) {
+ if (this.state == 'ended') return;
+
this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
this.state = 'connecting';
};
@@ -214,11 +231,19 @@ angular.module('MatrixCall', [])
var content = {
version: 0,
call_id: this.call_id,
- offer: description
+ offer: description,
+ lifetime: MatrixCall.CALL_TIMEOUT
};
this.sendEventWithRetry('m.call.invite', content);
var self = this;
+ $timeout(function() {
+ if (self.state == 'invite_sent') {
+ self.hangupReason = 'invite_timeout';
+ self.hangup();
+ }
+ }, MatrixCall.CALL_TIMEOUT);
+
$rootScope.$apply(function() {
self.state = 'invite_sent';
});
@@ -370,5 +395,53 @@ angular.module('MatrixCall', [])
}, delayMs);
};
+ // Sends candidates with are sent in a special way because we try to amalgamate them into one message
+ MatrixCall.prototype.sendCandidate = function(content) {
+ this.candidateSendQueue.push(content);
+ var self = this;
+ if (this.candidateSendTries == 0) $timeout(function() { self.sendCandidateQueue(); }, 100);
+ };
+
+ MatrixCall.prototype.sendCandidateQueue = function(content) {
+ if (this.candidateSendQueue.length == 0) return;
+
+ var cands = this.candidateSendQueue;
+ this.candidateSendQueue = [];
+ ++this.candidateSendTries;
+ var content = {
+ version: 0,
+ call_id: this.call_id,
+ candidates: cands
+ };
+ var self = this;
+ console.log("Attempting to send "+cands.length+" candidates");
+ matrixService.sendEvent(self.room_id, 'm.call.candidates', undefined, content).then(function() { self.candsSent(); }, function(error) { self.candsSendFailed(cands, error); } );
+ };
+
+ MatrixCall.prototype.candsSent = function() {
+ this.candidateSendTries = 0;
+ this.sendCandidateQueue();
+ };
+
+ MatrixCall.prototype.candsSendFailed = function(cands, error) {
+ for (var i = 0; i < cands.length; ++i) {
+ this.candidateSendQueue.push(cands[i]);
+ }
+
+ if (this.candidateSendTries > 5) {
+ console.log("Failed to send candidates on attempt "+ev.tries+". Giving up for now.");
+ this.candidateSendTries = 0;
+ return;
+ }
+
+ var delayMs = 500 * Math.pow(2, this.candidateSendTries);
+ ++this.candidateSendTries;
+ console.log("Failed to send candidates. Retrying in "+delayMs+"ms");
+ var self = this;
+ $timeout(function() {
+ self.sendCandidateQueue();
+ }, delayMs);
+ };
+
return MatrixCall;
}]);
diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js
index 015a88bcad..8b168cdedb 100644
--- a/webclient/components/matrix/matrix-filter.js
+++ b/webclient/components/matrix/matrix-filter.js
@@ -26,72 +26,74 @@ angular.module('matrixFilter', [])
// If there is an alias, use it
// TODO: only one alias is managed for now
var alias = matrixService.getRoomIdToAliasMapping(room_id);
- if (alias) {
- roomName = alias;
- }
-
- if (undefined === roomName) {
- var room = $rootScope.events.rooms[room_id];
- if (room) {
- // Get name from room state date
- var room_name_event = room["m.room.name"];
- if (room_name_event) {
- roomName = room_name_event.content.name;
- }
- else if (room.members) {
- // Else, build the name from its users
- // FIXME: Is it still required?
- // Limit the room renaming to 1:1 room
- if (2 === Object.keys(room.members).length) {
- for (var i in room.members) {
- var member = room.members[i];
- if (member.state_key !== matrixService.config().user_id) {
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ // Get name from room state date
+ var room_name_event = room["m.room.name"];
+ if (room_name_event) {
+ roomName = room_name_event.content.name;
+ }
+ else if (alias) {
+ roomName = alias;
+ }
+ else if (room.members) {
+ // Else, build the name from its users
+ // FIXME: Is it still required?
+ // Limit the room renaming to 1:1 room
+ if (2 === Object.keys(room.members).length) {
+ for (var i in room.members) {
+ var member = room.members[i];
+ if (member.state_key !== matrixService.config().user_id) {
- if (member.state_key in $rootScope.presence) {
- // If the user is available in presence, use the displayname there
- // as it is the most uptodate
- roomName = $rootScope.presence[member.state_key].content.displayname;
- }
- else if (member.content.displayname) {
- roomName = member.content.displayname;
- }
- else {
- roomName = member.state_key;
- }
+ if (member.state_key in $rootScope.presence) {
+ // If the user is available in presence, use the displayname there
+ // as it is the most uptodate
+ roomName = $rootScope.presence[member.state_key].content.displayname;
}
- }
- }
- else if (1 === Object.keys(room.members).length) {
- // The other member may be in the invite list, get all invited users
- var invitedUserIDs = [];
- for (var i in room.messages) {
- var message = room.messages[i];
- if ("m.room.member" === message.type && "invite" === message.membership) {
- // Make sure there is no duplicate user
- if (-1 === invitedUserIDs.indexOf(message.state_key)) {
- invitedUserIDs.push(message.state_key);
- }
- }
- }
-
- // For now, only 1:1 room needs to be renamed. It means only 1 invited user
- if (1 === invitedUserIDs.length) {
- var userID = invitedUserIDs[0];
-
- // Try to resolve his displayname in presence global data
- if (userID in $rootScope.presence) {
- roomName = $rootScope.presence[userID].content.displayname;
+ else if (member.content.displayname) {
+ roomName = member.content.displayname;
}
else {
- roomName = userID;
+ roomName = member.state_key;
}
}
}
}
+ else if (1 === Object.keys(room.members).length) {
+ // The other member may be in the invite list, get all invited users
+ var invitedUserIDs = [];
+ for (var i in room.messages) {
+ var message = room.messages[i];
+ if ("m.room.member" === message.type && "invite" === message.membership) {
+ // Make sure there is no duplicate user
+ if (-1 === invitedUserIDs.indexOf(message.state_key)) {
+ invitedUserIDs.push(message.state_key);
+ }
+ }
+ }
+
+ // For now, only 1:1 room needs to be renamed. It means only 1 invited user
+ if (1 === invitedUserIDs.length) {
+ var userID = invitedUserIDs[0];
+
+ // Try to resolve his displayname in presence global data
+ if (userID in $rootScope.presence) {
+ roomName = $rootScope.presence[userID].content.displayname;
+ }
+ else {
+ roomName = userID;
+ }
+ }
+ }
}
}
+ // Always show the alias in the room displayed name
+ if (roomName && alias && alias !== roomName) {
+ roomName += " (" + alias + ")";
+ }
+
if (undefined === roomName) {
// By default, use the room ID
roomName = room_id;
diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js
index b0dcf19100..d05eecf72a 100644
--- a/webclient/components/matrix/matrix-phone-service.js
+++ b/webclient/components/matrix/matrix-phone-service.js
@@ -24,22 +24,52 @@ angular.module('matrixPhoneService', [])
matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT";
matrixPhoneService.allCalls = {};
+ // a place to save candidates that come in for calls we haven't got invites for yet (when paginating backwards)
+ matrixPhoneService.candidatesByCall = {};
matrixPhoneService.callPlaced = function(call) {
matrixPhoneService.allCalls[call.call_id] = call;
};
$rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
- if (!isLive) return; // until matrix supports expiring messages
if (event.user_id == matrixService.config().user_id) return;
+
var msg = event.content;
+
if (event.type == 'm.call.invite') {
+ if (event.age == undefined || msg.lifetime == undefined) {
+ // if the event doesn't have either an age (the HS is too old) or a lifetime
+ // (the sending client was too old when it sent it) then fall back to old behaviour
+ if (!isLive) return; // until matrix supports expiring messages
+ }
+
+ if (event.age > msg.lifetime) {
+ console.log("Ignoring expired call event of type "+event.type);
+ return;
+ }
+
+ var call = undefined;
+ if (!isLive) {
+ // if this event wasn't live then this call may already be over
+ call = matrixPhoneService.allCalls[msg.call_id];
+ if (call && call.state == 'ended') {
+ return;
+ }
+ }
+
var MatrixCall = $injector.get('MatrixCall');
var call = new MatrixCall(event.room_id);
call.call_id = msg.call_id;
- call.initWithInvite(msg);
+ call.initWithInvite(event);
matrixPhoneService.allCalls[call.call_id] = call;
+ // if we stashed candidate events for that call ID, play them back now
+ if (!isLive && matrixPhoneService.candidatesByCall[call.call_id] != undefined) {
+ for (var i = 0; i < matrixPhoneService.candidatesByCall[call.call_id].length; ++i) {
+ call.gotRemoteIceCandidate(matrixPhoneService.candidatesByCall[call.call_id][i]);
+ }
+ }
+
// Were we trying to call that user (room)?
var existingCall;
var callIds = Object.keys(matrixPhoneService.allCalls);
@@ -77,21 +107,37 @@ angular.module('matrixPhoneService', [])
return;
}
call.receivedAnswer(msg);
- } else if (event.type == 'm.call.candidate') {
+ } else if (event.type == 'm.call.candidates') {
var call = matrixPhoneService.allCalls[msg.call_id];
- if (!call) {
- console.log("Got candidate for unknown call ID "+msg.call_id);
+ if (!call && isLive) {
+ console.log("Got candidates for unknown call ID "+msg.call_id);
return;
+ } else if (!call) {
+ if (matrixPhoneService.candidatesByCall[msg.call_id] == undefined) {
+ matrixPhoneService.candidatesByCall[msg.call_id] = [];
+ }
+ matrixPhoneService.candidatesByCall[msg.call_id] = matrixPhoneService.candidatesByCall[msg.call_id].concat(msg.candidates);
+ } else {
+ for (var i = 0; i < msg.candidates.length; ++i) {
+ call.gotRemoteIceCandidate(msg.candidates[i]);
+ }
}
- call.gotRemoteIceCandidate(msg.candidate);
} else if (event.type == 'm.call.hangup') {
var call = matrixPhoneService.allCalls[msg.call_id];
- if (!call) {
+ if (!call && isLive) {
console.log("Got hangup for unknown call ID "+msg.call_id);
- return;
+ } else if (!call) {
+ // if not live, store the fact that the call has ended because we're probably getting events backwards so
+ // the hangup will come before the invite
+ var MatrixCall = $injector.get('MatrixCall');
+ var call = new MatrixCall(event.room_id);
+ call.call_id = msg.call_id;
+ call.initWithHangup(event);
+ matrixPhoneService.allCalls[msg.call_id] = call;
+ } else {
+ call.onHangupReceived();
+ delete(matrixPhoneService.allCalls[msg.call_id]);
}
- call.onHangupReceived();
- delete(matrixPhoneService.allCalls[msg.call_id]);
}
});
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index 68ef16800b..069e02e939 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -81,38 +81,155 @@ angular.module('matrixService', [])
return $http(request);
};
+
+ var doRegisterLogin = function(path, loginType, sessionId, userName, password, threepidCreds) {
+ var data = {};
+ if (loginType === "m.login.recaptcha") {
+ var challengeToken = Recaptcha.get_challenge();
+ var captchaEntry = Recaptcha.get_response();
+ data = {
+ type: "m.login.recaptcha",
+ challenge: challengeToken,
+ response: captchaEntry
+ };
+ }
+ else if (loginType === "m.login.email.identity") {
+ data = {
+ threepidCreds: threepidCreds
+ };
+ }
+ else if (loginType === "m.login.password") {
+ data = {
+ user: userName,
+ password: password
+ };
+ }
+
+ if (sessionId) {
+ data.session = sessionId;
+ }
+ data.type = loginType;
+ console.log("doRegisterLogin >>> " + loginType);
+ return doRequest("POST", path, undefined, data);
+ };
return {
/****** Home server API ******/
prefix: prefixPath,
// Register an user
- register: function(user_name, password, threepidCreds, useCaptcha) {
- // The REST path spec
+ register: function(user_name, password, threepidCreds, useCaptcha) {
+ // registration is composed of multiple requests, to check you can
+ // register, then to actually register. This deferred will fire when
+ // all the requests are done, along with the final response.
+ var deferred = $q.defer();
var path = "/register";
- var data = {
- user_id: user_name,
- password: password,
- threepidCreds: threepidCreds
- };
+ // check we can actually register with this HS.
+ doRequest("GET", path, undefined, undefined).then(
+ function(response) {
+ console.log("/register [1] : "+JSON.stringify(response));
+ var flows = response.data.flows;
+ var knownTypes = [
+ "m.login.password",
+ "m.login.recaptcha",
+ "m.login.email.identity"
+ ];
+ // if they entered 3pid creds, we want to use a flow which uses it.
+ var useThreePidFlow = threepidCreds != undefined;
+ var flowIndex = 0;
+ var firstRegType = undefined;
+
+ for (var i=0; i<flows.length; i++) {
+ var isThreePidFlow = false;
+ if (flows[i].stages) {
+ for (var j=0; j<flows[i].stages.length; j++) {
+ var regType = flows[i].stages[j];
+ if (knownTypes.indexOf(regType) === -1) {
+ deferred.reject("Unknown type: "+regType);
+ return;
+ }
+ if (regType == "m.login.email.identity") {
+ isThreePidFlow = true;
+ }
+ if (!useCaptcha && regType == "m.login.recaptcha") {
+ console.error("Web client setup to not use captcha, but HS demands a captcha.");
+ deferred.reject({
+ data: {
+ errcode: "M_CAPTCHA_NEEDED",
+ error: "Home server requires a captcha."
+ }
+ });
+ return;
+ }
+ }
+ }
+
+ if ( (isThreePidFlow && useThreePidFlow) || (!isThreePidFlow && !useThreePidFlow) ) {
+ flowIndex = i;
+ }
+
+ if (knownTypes.indexOf(flows[i].type) == -1) {
+ deferred.reject("Unknown type: "+flows[i].type);
+ return;
+ }
+ }
+
+ // looks like we can register fine, go ahead and do it.
+ console.log("Using flow " + JSON.stringify(flows[flowIndex]));
+ firstRegType = flows[flowIndex].type;
+ var sessionId = undefined;
+
+ // generic response processor so it can loop as many times as required
+ var loginResponseFunc = function(response) {
+ if (response.data.session) {
+ sessionId = response.data.session;
+ }
+ console.log("login response: " + JSON.stringify(response.data));
+ if (response.data.access_token) {
+ deferred.resolve(response);
+ }
+ else if (response.data.next) {
+ var nextType = response.data.next;
+ if (response.data.next instanceof Array) {
+ for (var i=0; i<response.data.next.length; i++) {
+ if (useThreePidFlow && response.data.next[i] == "m.login.email.identity") {
+ nextType = response.data.next[i];
+ break;
+ }
+ else if (!useThreePidFlow && response.data.next[i] != "m.login.email.identity") {
+ nextType = response.data.next[i];
+ break;
+ }
+ }
+ }
+ return doRegisterLogin(path, nextType, sessionId, user_name, password, threepidCreds).then(
+ loginResponseFunc,
+ function(err) {
+ deferred.reject(err);
+ }
+ );
+ }
+ else {
+ deferred.reject("Unknown continuation: "+JSON.stringify(response));
+ }
+ };
+
+ // set the ball rolling
+ doRegisterLogin(path, firstRegType, undefined, user_name, password, threepidCreds).then(
+ loginResponseFunc,
+ function(err) {
+ deferred.reject(err);
+ }
+ );
+
+ },
+ function(err) {
+ deferred.reject(err);
+ }
+ );
- if (useCaptcha) {
- // Not all home servers will require captcha on signup, but if this flag is checked,
- // send captcha information.
- // TODO: Might be nice to make this a bit more flexible..
- var challengeToken = Recaptcha.get_challenge();
- var captchaEntry = Recaptcha.get_response();
- var captchaType = "m.login.recaptcha";
-
- data.captcha = {
- type: captchaType,
- challenge: challengeToken,
- response: captchaEntry
- };
- }
-
- return doRequest("POST", path, undefined, data);
+ return deferred.promise;
},
// Create a room
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
index c0c4ea11aa..e35219bebb 100644
--- a/webclient/home/home-controller.js
+++ b/webclient/home/home-controller.js
@@ -53,6 +53,8 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
// Add room_alias & room_display_name members
angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
+
+ eventHandlerService.setRoomVisibility(room.room_id, "public");
}
}
);
@@ -117,6 +119,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..d948205e19 100644
--- a/webclient/recents/recents-filter.js
+++ b/webclient/recents/recents-filter.js
@@ -17,31 +17,47 @@
'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
+}]);
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index 3d736b6694..edfc1677eb 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">
+ <td ng-class="room['m.room.join_rules'].content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : '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>
@@ -22,70 +26,70 @@
<td colspan="3" class="recentsRoomSummary">
<div ng-show="room.membership === 'invite'">
- {{ room.lastMsg.inviter | mUserDisplayName: room.room_id }} invited you
+ {{ 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..de50058743 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;
@@ -32,7 +32,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
can_paginate: false, // this is toggled off when we are not ready yet to paginate or when we run out of items
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
stream_failure: undefined, // the response when the stream fails
- waiting_for_joined_event: false // true when the join request is pending. Back to false once the corresponding m.room.member event is received
+ waiting_for_joined_event: false, // true when the join request is pending. Back to false once the corresponding m.room.member event is received
+ messages_visibility: "hidden" // In order to avoid flickering when scrolling down the message table at the page opening, delay the message table display
};
$scope.members = {};
$scope.autoCompleting = false;
@@ -53,8 +54,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
return;
};
- // Use the filter applied in html to set the input value
- $scope.name.newNameText = $filter('mRoomName')($scope.room_id);
+ var nameEvent = $rootScope.events.rooms[$scope.room_id]['m.room.name'];
+ if (nameEvent) {
+ $scope.name.newNameText = nameEvent.content.name;
+ }
+ else {
+ $scope.name.newNameText = "";
+ }
// Force focus to the input
$timeout(function() {
@@ -131,6 +137,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$timeout(function() {
objDiv.scrollTop = objDiv.scrollHeight;
+
+ // Show the message table once the first scrolldown is done
+ if ("visible" !== $scope.state.messages_visibility) {
+ $timeout(function() {
+ $scope.state.messages_visibility = "visible";
+ }, 0);
+ }
}, 0);
}
};
@@ -139,27 +152,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 +174,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 +246,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.
@@ -406,12 +417,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
};
$scope.send = function() {
- if ($scope.textInput === "") {
+ if (undefined === $scope.textInput || $scope.textInput === "") {
return;
}
scrollToBottom(true);
-
+
+ // Store the command in the history
+ history.push($scope.textInput);
+
var promise;
var cmd;
var args;
@@ -676,6 +690,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 +702,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
@@ -729,7 +751,10 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
// Make recents highlight the current room
$scope.recentsSelectedRoomID = $scope.room_id;
- // Get the up-to-date the current member list
+ // Init the history for this room
+ history.init();
+
+ // Get the up-to-date the current member list
matrixService.getMemberList($scope.room_id).then(
function(response) {
for (var i = 0; i < response.data.chunk.length; i++) {
@@ -743,9 +768,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;
@@ -832,4 +866,82 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$rootScope.currentCall = call;
};
+ // Manage history of typed messages
+ // History is saved in sessionStoratge so that it survives when the user
+ // navigates through the rooms and when it refreshes the page
+ var history = {
+ // The list of typed messages. Index 0 is the more recents
+ data: [],
+
+ // The position in the history currently displayed
+ position: -1,
+
+ // The message the user has started to type before going into the history
+ typingMessage: undefined,
+
+ // Init/load data for the current room
+ init: function() {
+ var data = sessionStorage.getItem("history_" + $scope.room_id);
+ if (data) {
+ this.data = JSON.parse(data);
+ }
+ },
+
+ // Store a message in the history
+ push: function(message) {
+ this.data.unshift(message);
+
+ // Update the session storage
+ sessionStorage.setItem("history_" + $scope.room_id, JSON.stringify(this.data));
+
+ // Reset history position
+ this.position = -1;
+ this.typingMessage = undefined;
+ },
+
+ // Move in the history
+ go: function(offset) {
+
+ if (-1 === this.position) {
+ // User starts to go to into the history, save the current line
+ this.typingMessage = $scope.textInput;
+ }
+ else {
+ // If the user modified this line in history, keep the change
+ this.data[this.position] = $scope.textInput;
+ }
+
+ // Bounds the new position to valid data
+ var newPosition = this.position + offset;
+ newPosition = Math.max(-1, newPosition);
+ newPosition = Math.min(newPosition, this.data.length - 1);
+ this.position = newPosition;
+
+ if (-1 !== this.position) {
+ // Show the message from the history
+ $scope.textInput = this.data[this.position];
+ }
+ else if (undefined !== this.typingMessage) {
+ // Go back to the message the user started to type
+ $scope.textInput = this.typingMessage;
+ }
+ }
+ };
+
+ // Make history singleton methods available from HTML
+ $scope.history = {
+ goUp: function($event) {
+ if ($scope.room_id) {
+ history.go(1);
+ }
+ $event.preventDefault();
+ },
+ goDown: function($event) {
+ if ($scope.room_id) {
+ history.go(-1);
+ }
+ $event.preventDefault();
+ }
+ };
+
}]);
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 9d617eadd8..44a0e34d9f 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -9,7 +9,7 @@
{{ room_id | mRoomName }}
</div>
<form ng-submit="name.updateName()" ng-show="name.isEditing" class="roomNameForm">
- <input ng-model="name.newNameText" ng-blur="name.cancelEdit()" class="roomNameInput" />
+ <input ng-model="name.newNameText" ng-blur="name.cancelEdit()" class="roomNameInput" placeholder="Room name"/>
</form>
</div>
@@ -23,7 +23,7 @@
{{ events.rooms[room_id]['m.room.topic'].content.topic | limitTo: 200}}
</div>
<form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
- <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" />
+ <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" placeholder="Topic"/>
</form>
</div>
</div>
@@ -56,7 +56,10 @@
</table>
</div>
- <div id="messageTableWrapper" ng-hide="state.permission_denied" keep-scroll>
+ <div id="messageTableWrapper"
+ ng-hide="state.permission_denied"
+ ng-style="{ 'visibility': state.messages_visibility }"
+ keep-scroll>
<!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below -->
<table id="messageTable" infinite-scroll="paginateMore()">
<tr ng-repeat="msg in events.rooms[room_id].messages"
@@ -105,7 +108,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>
@@ -156,7 +159,8 @@
<td width="*">
<textarea id="mainInput" rows="1" ng-model="textInput" ng-enter="send()"
ng-disabled="state.permission_denied"
- ng-focus="true" autocomplete="off" tab-complete/>
+ ng-keydown="(38 === $event.which) ? history.goUp($event) : ((40 === $event.which) ? history.goDown($event) : 0)"
+ ng-focus="true" autocomplete="off" tab-complete/>
</td>
<td id="buttonsCell">
<button ng-click="send()" ng-disabled="state.permission_denied">Send</button>
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/>
|