diff --git a/webclient/CAPTCHA_SETUP b/syweb/webclient/CAPTCHA_SETUP
index ebc8a5f3b0..ebc8a5f3b0 100644
--- a/webclient/CAPTCHA_SETUP
+++ b/syweb/webclient/CAPTCHA_SETUP
diff --git a/webclient/README b/syweb/webclient/README
index ef79b25708..ef79b25708 100644
--- a/webclient/README
+++ b/syweb/webclient/README
diff --git a/webclient/app-controller.js b/syweb/webclient/app-controller.js
index e4b7cd286f..582c075e3d 100644
--- a/webclient/app-controller.js
+++ b/syweb/webclient/app-controller.js
@@ -21,18 +21,12 @@ limitations under the License.
'use strict';
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
-.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService',
- function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, eventHandlerService, matrixPhoneService) {
+.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', 'matrixService', 'mPresence', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService', 'modelService',
+ function($scope, $location, $rootScope, $timeout, matrixService, mPresence, eventStreamService, eventHandlerService, matrixPhoneService, modelService) {
// Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path();
- // disable nganimate for the local and remote video elements because ngAnimate appears
- // to be buggy and leaves animation classes on the video elements causing them to show
- // when they should not (their animations are pure CSS3)
- $animate.enabled(false, angular.element('#localVideo'));
- $animate.enabled(false, angular.element('#remoteVideo'));
-
// Update the location state when the ng location changed
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
$scope.location = $location.path();
@@ -112,12 +106,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
if (!$rootScope.currentCall) {
// This causes the still frame to be flushed out of the video elements,
// avoiding a flash of the last frame of the previous call when starting the next
- angular.element('#localVideo')[0].load();
- angular.element('#remoteVideo')[0].load();
+ if (angular.element('#localVideo')[0].load) angular.element('#localVideo')[0].load();
+ if (angular.element('#remoteVideo')[0].load) angular.element('#remoteVideo')[0].load();
return;
}
- var roomMembers = angular.copy($rootScope.events.rooms[$rootScope.currentCall.room_id].members);
+ var roomMembers = angular.copy(modelService.getRoom($rootScope.currentCall.room_id).current_room_state.members);
delete roomMembers[matrixService.config().user_id];
$rootScope.currentCall.user_id = Object.keys(roomMembers)[0];
@@ -187,8 +181,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
}
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
- call.localVideoElement = angular.element('#localVideo')[0];
- call.remoteVideoElement = angular.element('#remoteVideo')[0];
+ call.localVideoSelector = '#localVideo';
+ call.remoteVideoSelector = '#remoteVideo';
$rootScope.currentCall = call;
});
diff --git a/webclient/app-directive.js b/syweb/webclient/app-directive.js
index 75283598ab..c1ba0af3a9 100644
--- a/webclient/app-directive.js
+++ b/syweb/webclient/app-directive.js
@@ -40,4 +40,45 @@ angular.module('matrixWebClient')
}
}
};
-}]);
\ No newline at end of file
+}])
+.directive('asjson', function() {
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function (scope, element, attrs, ngModelCtrl) {
+ function isValidJson(model) {
+ var flag = true;
+ try {
+ angular.fromJson(model);
+ } catch (err) {
+ flag = false;
+ }
+ return flag;
+ };
+
+ function string2JSON(text) {
+ try {
+ var j = angular.fromJson(text);
+ ngModelCtrl.$setValidity('json', true);
+ return j;
+ } catch (err) {
+ //returning undefined results in a parser error as of angular-1.3-rc.0, and will not go through $validators
+ //return undefined
+ ngModelCtrl.$setValidity('json', false);
+ return text;
+ }
+ };
+
+ function JSON2String(object) {
+ return angular.toJson(object, true);
+ };
+
+ //$validators is an object, where key is the error
+ //ngModelCtrl.$validators.json = isValidJson;
+
+ //array pipelines
+ ngModelCtrl.$parsers.push(string2JSON);
+ ngModelCtrl.$formatters.push(JSON2String);
+ }
+ }
+});
diff --git a/webclient/app-filter.js b/syweb/webclient/app-filter.js
index 39ea1d637d..65da0d312d 100644
--- a/webclient/app-filter.js
+++ b/syweb/webclient/app-filter.js
@@ -29,10 +29,10 @@ angular.module('matrixWebClient')
return s + "s";
}
if (t < 60 * 60) {
- return m + "m "; // + s + "s";
+ return m + "m"; // + s + "s";
}
if (t < 24 * 60 * 60) {
- return h + "h "; // + m + "m";
+ return h + "h"; // + m + "m";
}
return d + "d "; // + h + "h";
};
diff --git a/webclient/app.css b/syweb/webclient/app.css
index 20a13aad81..25f7208a11 100755
--- a/webclient/app.css
+++ b/syweb/webclient/app.css
@@ -66,18 +66,15 @@ textarea, input {
margin-left: 4px;
margin-right: 4px;
margin-top: 8px;
+ transition: transform linear 0.5s;
+ transition: -webkit-transform linear 0.5s;
}
-#callEndedIcon {
- transition:all linear 0.5s;
-}
-
-#callEndedIcon {
+.callIcon.ended {
transform: rotateZ(45deg);
-}
-
-#callEndedIcon.ng-hide {
- transform: rotateZ(0deg);
+ -webkit-transform: rotateZ(45deg);
+ filter: hue-rotate(-90deg);
+ -webkit-filter: hue-rotate(-90deg);
}
#callPeerImage {
@@ -136,17 +133,17 @@ textarea, input {
transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms;
}
-#localVideo.mini {
+.mini #localVideo {
top: 0px;
left: 130px;
}
-#localVideo.large {
+.large #localVideo {
top: 70px;
left: 20px;
}
-#localVideo.ended {
+.ended #localVideo {
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
@@ -157,19 +154,19 @@ textarea, input {
transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms;
}
-#remoteVideo.mini {
+.mini #remoteVideo {
left: 260px;
top: 0px;
width: 128px;
}
-#remoteVideo.large {
+.large #remoteVideo {
left: 0px;
top: 50px;
width: 100%;
}
-#remoteVideo.ended {
+.ended #remoteVideo {
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
@@ -318,7 +315,7 @@ textarea, input {
position: absolute;
bottom: 0px;
width: 100%;
- height: 100px;
+ height: 70px;
background-color: #f8f8f8;
border-top: #aaa 1px solid;
}
@@ -326,7 +323,9 @@ textarea, input {
#controls {
max-width: 1280px;
padding: 12px;
+ padding-right: 42px;
margin: auto;
+ position: relative;
}
#buttonsCell {
@@ -343,7 +342,19 @@ textarea, input {
#mainInput {
width: 100%;
- resize: none;
+ padding: 5px;
+ resize: vertical;
+}
+
+#attachButton {
+ position: absolute;
+ cursor: pointer;
+ margin-top: 3px;
+ right: 0px;
+ background: url('img/attach.png');
+ width: 25px;
+ height: 25px;
+ border: 0px;
}
.blink {
@@ -415,18 +426,72 @@ textarea, input {
.roomHeaderInfo {
text-align: right;
float: right;
- margin-top: 15px;
+ margin-top: 0px;
+ margin-right: 30px;
+}
+
+/*** Room Info Dialog ***/
+
+.room-info {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+.room-info-event {
+ border-bottom: 1pt solid black;
+}
+
+.room-info-event-meta {
+ padding-top: 1em;
+ padding-bottom: 1em;
+}
+
+.room-info-event-content {
+ padding-top: 1em;
+ padding-bottom: 1em;
+}
+
+.monospace {
+ font-family: monospace;
+}
+
+.redact-button {
+ float: left
+}
+
+.room-info-textarea-content {
+ height: auto;
+ width: 100%;
+ resize: vertical;
+}
+
+/*** Control Buttons ***/
+#controlButtons {
+ float: right;
+ margin-right: -4px;
+ padding-bottom: 6px;
+}
+
+.controlButton {
+ cursor: pointer;
+ border: 0px;
+ width: 30px;
+ height: 30px;
+ margin-left: 3px;
+ margin-right: 3px;
}
/*** Participant list ***/
#usersTableWrapper {
float: right;
- width: 120px;
+ clear: right;
+ width: 101px;
height: 100%;
overflow-y: auto;
}
+/*
#usersTable {
width: 100%;
border-collapse: collapse;
@@ -442,36 +507,66 @@ textarea, input {
position: relative;
background-color: #000;
}
+*/
-.userAvatar .userAvatarImage {
- position: absolute;
- top: 0px;
+.userAvatar {
+}
+
+.userAvatarFrame {
+ border-radius: 46px;
+ width: 80px;
+ margin: auto;
+ position: relative;
+ border: 3px solid #aaa;
+ background-color: #aaa;
+}
+
+.userAvatarImage {
+ border-radius: 40px;
+ text-align: center;
object-fit: cover;
- width: 100%;
+ display: block;
}
+/*
.userAvatar .userAvatarGradient {
position: absolute;
bottom: 20px;
width: 100%;
}
+*/
-.userAvatar .userName {
- position: absolute;
- color: #fff;
- margin: 2px;
- bottom: 0px;
+.userName {
+ margin-top: 3px;
+ margin-bottom: 6px;
+ text-align: center;
font-size: 12px;
- word-break: break-all;
+ word-wrap: break-word;
}
-.userAvatar .userPowerLevel {
+.userPowerLevel {
position: absolute;
+ bottom: -1px;
+ height: 1px;
+ background-color: #f00;
+}
+
+.userPowerLevelBar {
+ display: inline;
+ position: absolute;
+ width: 2px;
+ height: 10px;
+/* border: 1px solid #000;
+*/ background-color: #aaa;
+}
+
+.userPowerLevelMeter {
+ position: relative;
bottom: 0px;
- height: 2px;
background-color: #f00;
}
+/*
.userPresence {
text-align: center;
font-size: 12px;
@@ -479,12 +574,15 @@ textarea, input {
background-color: #aaa;
border-bottom: 1px #ddd solid;
}
+*/
.online {
+ border-color: #38AF00;
background-color: #38AF00;
}
.unavailable {
+ border-color: #FFCC00;
background-color: #FFCC00;
}
@@ -507,18 +605,21 @@ textarea, input {
#messageTable td {
padding: 0px;
+/* border: 1px solid #888; */
}
.leftBlock {
- width: 14em;
+ width: 7em;
word-wrap: break-word;
vertical-align: top;
background-color: #fff;
- color: #888;
+ color: #aaa;
font-weight: medium;
font-size: 12px;
text-align: right;
+/*
border-top: 1px #ddd solid;
+*/
}
.rightBlock {
@@ -529,13 +630,24 @@ textarea, input {
}
.sender, .timestamp {
- padding-right: 1em;
- padding-left: 1em;
- padding-top: 3px;
+/* padding-top: 3px;
+*/}
+
+.timestamp {
+ font-size: 10px;
+ color: #ccc;
+ height: 13px;
+ margin-top: 4px;
+ transition-property: opacity;
+ transition-duration: 0.3s;
}
.sender {
- margin-bottom: -3px;
+ font-size: 12px;
+/*
+ margin-top: 5px;
+ margin-bottom: -9px;
+*/
}
.avatar {
@@ -546,7 +658,11 @@ textarea, input {
}
.avatarImage {
+ position: relative;
+ top: 5px;
object-fit: cover;
+ border-radius: 32px;
+ margin-top: 4px;
}
.emote {
@@ -560,6 +676,7 @@ textarea, input {
}
.image {
+ border: 1px solid #888;
display: block;
max-width:320px;
max-height:320px;
@@ -572,19 +689,23 @@ textarea, input {
}
.bubble {
+/*
background-color: #eee;
border: 1px solid #d8d8d8;
- display: inline-block;
margin-bottom: -1px;
- max-width: 90%;
- font-size: 14px;
- word-wrap: break-word;
padding-top: 7px;
padding-bottom: 5px;
+ -webkit-text-size-adjust:100%
+ vertical-align: middle;
+*/
+ display: inline-block;
+ max-width: 90%;
padding-left: 1em;
padding-right: 1em;
- vertical-align: middle;
- -webkit-text-size-adjust:100%
+ padding-top: 2px;
+ padding-bottom: 2px;
+ font-size: 14px;
+ word-wrap: break-word;
}
.bubble img {
@@ -592,8 +713,8 @@ textarea, input {
max-height: auto;
}
-.differentUser td {
- padding-bottom: 5px ! important;
+.differentUser .msg {
+ padding-top: 14px ! important;
}
.mine {
@@ -604,13 +725,15 @@ textarea, input {
.text.membership .bubble,
.mine .text.emote .bubble,
.mine .text.membership .bubble
- {
+{
background-color: transparent ! important;
border: 0px ! important;
}
.mine .text .bubble {
+/*
background-color: #f8f8ff ! important;
+*/
text-align: left ! important;
}
@@ -670,6 +793,8 @@ textarea, input {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
}
.recentsRoom {
@@ -684,6 +809,14 @@ textarea, input {
background-color: #eee;
}
+.recentsRoomUnread {
+ background-color: #fee;
+}
+
+.recentsRoomBing {
+ background-color: #eef;
+}
+
.recentsRoomName {
font-size: 16px;
padding-top: 7px;
@@ -720,7 +853,7 @@ textarea, input {
padding-right: 10px;
margin-right: 10px;
height: 100%;
- border-right: 1px solid #ddd;
+/* border-right: 1px solid #ddd; */
overflow-y: auto;
}
diff --git a/webclient/app.js b/syweb/webclient/app.js
index 099e2170a0..9e5b85820d 100644
--- a/webclient/app.js
+++ b/syweb/webclient/app.js
@@ -16,7 +16,6 @@ limitations under the License.
var matrixWebClient = angular.module('matrixWebClient', [
'ngRoute',
- 'ngAnimate',
'MatrixWebClientController',
'LoginController',
'RegisterController',
@@ -30,8 +29,13 @@ var matrixWebClient = angular.module('matrixWebClient', [
'MatrixCall',
'eventStreamService',
'eventHandlerService',
+ 'notificationService',
+ 'recentsService',
+ 'modelService',
+ 'commandsService',
'infinite-scroll',
- 'ui.bootstrap'
+ 'ui.bootstrap',
+ 'monospaced.elastic'
]);
matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
diff --git a/webclient/bootstrap.css b/syweb/webclient/bootstrap.css
index 7ebcb2a007..7ebcb2a007 100644
--- a/webclient/bootstrap.css
+++ b/syweb/webclient/bootstrap.css
diff --git a/webclient/components/fileInput/file-input-directive.js b/syweb/webclient/components/fileInput/file-input-directive.js
index 9c849a140f..9c849a140f 100644
--- a/webclient/components/fileInput/file-input-directive.js
+++ b/syweb/webclient/components/fileInput/file-input-directive.js
diff --git a/webclient/components/fileUpload/file-upload-service.js b/syweb/webclient/components/fileUpload/file-upload-service.js
index e0f67b2c6c..b544e29509 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/syweb/webclient/components/fileUpload/file-upload-service.js
@@ -64,7 +64,8 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
var imageMessage = {
msgtype: "m.image",
url: undefined,
- body: {
+ body: "Image",
+ info: {
size: undefined,
w: undefined,
h: undefined,
@@ -90,7 +91,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
function(url) {
// Update message metadata
imageMessage.url = url;
- imageMessage.body = {
+ imageMessage.info = {
size: imageFile.size,
w: size.width,
h: size.height,
@@ -101,7 +102,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
// reuse the original image info for thumbnail data
if (!imageMessage.thumbnail_url) {
imageMessage.thumbnail_url = imageMessage.url;
- imageMessage.thumbnail_info = imageMessage.body;
+ imageMessage.thumbnail_info = imageMessage.info;
}
// We are done
diff --git a/syweb/webclient/components/matrix/commands-service.js b/syweb/webclient/components/matrix/commands-service.js
new file mode 100644
index 0000000000..3c516ad1e4
--- /dev/null
+++ b/syweb/webclient/components/matrix/commands-service.js
@@ -0,0 +1,164 @@
+/*
+Copyright 2014 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+/*
+This service contains logic for parsing and performing IRC style commands.
+*/
+angular.module('commandsService', [])
+.factory('commandsService', ['$q', '$location', 'matrixService', 'modelService', function($q, $location, matrixService, modelService) {
+
+ // create a rejected promise with the given message
+ var reject = function(msg) {
+ var deferred = $q.defer();
+ deferred.reject({
+ data: {
+ error: msg
+ }
+ });
+ return deferred.promise;
+ };
+
+ // Change your nickname
+ var doNick = function(room_id, args) {
+ if (args) {
+ return matrixService.setDisplayName(args);
+ }
+ return reject("Usage: /nick <display_name>");
+ };
+
+ // Join a room
+ var doJoin = function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ var room_alias = matches[1];
+ $location.url("room/" + room_alias);
+ // NB: We don't need to actually do the join, since that happens
+ // automatically if we are not joined onto a room already when
+ // the page loads.
+ return reject("Joining "+room_alias);
+ }
+ }
+ return reject("Usage: /join <room_alias>");
+ };
+
+ // Kick a user from the room with an optional reason
+ var doKick = function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(.*))?$/);
+ if (matches) {
+ return matrixService.kick(room_id, matches[1], matches[3]);
+ }
+ }
+ return reject("Usage: /kick <userId> [<reason>]");
+ };
+
+ // Ban a user from the room with an optional reason
+ var doBan = function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(.*))?$/);
+ if (matches) {
+ return matrixService.ban(room_id, matches[1], matches[3]);
+ }
+ }
+ return reject("Usage: /ban <userId> [<reason>]");
+ };
+
+ // Unban a user from the room
+ var doUnban = function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ // Reset the user membership to "leave" to unban him
+ return matrixService.unban(room_id, matches[1]);
+ }
+ }
+ return reject("Usage: /unban <userId>");
+ };
+
+ // Define the power level of a user
+ var doOp = function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(\d+))?$/);
+ var powerLevel = 50; // default power level for op
+ if (matches) {
+ var user_id = matches[1];
+ if (matches.length === 4 && undefined !== matches[3]) {
+ powerLevel = parseInt(matches[3]);
+ }
+ if (powerLevel !== NaN) {
+ var powerLevelEvent = modelService.getRoom(room_id).current_room_state.state("m.room.power_levels");
+ return matrixService.setUserPowerLevel(room_id, user_id, powerLevel, powerLevelEvent);
+ }
+ }
+ }
+ return reject("Usage: /op <userId> [<power level>]");
+ };
+
+ // Reset the power level of a user
+ var doDeop = function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ var powerLevelEvent = modelService.getRoom(room_id).current_room_state.state("m.room.power_levels");
+ return matrixService.setUserPowerLevel(room_id, args, undefined, powerLevelEvent);
+ }
+ }
+ return reject("Usage: /deop <userId>");
+ };
+
+
+ var commands = {
+ "nick": doNick,
+ "join": doJoin,
+ "kick": doKick,
+ "ban": doBan,
+ "unban": doUnban,
+ "op": doOp,
+ "deop": doDeop
+ };
+
+ return {
+
+ /**
+ * Process the given text for commands and perform them.
+ * @param {String} roomId The room in which the input was performed.
+ * @param {String} input The raw text input by the user.
+ * @return {Promise} A promise of the pending command, or null if the
+ * input is not a command.
+ */
+ processInput: function(roomId, input) {
+ // trim any trailing whitespace, as it can confuse the parser for
+ // IRC-style commands
+ input = input.replace(/\s+$/, "");
+ if (input[0] === "/" && input[1] !== "/") {
+ var bits = input.match(/^(\S+?)( +(.*))?$/);
+ var cmd = bits[1].substring(1);
+ var args = bits[3];
+ if (commands[cmd]) {
+ return commands[cmd](roomId, args);
+ }
+ return reject("Unrecognised IRC-style command: " + cmd);
+ }
+ return null; // not a command
+ }
+
+ };
+
+}]);
+
diff --git a/syweb/webclient/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js
new file mode 100644
index 0000000000..efe7bf234c
--- /dev/null
+++ b/syweb/webclient/components/matrix/event-handler-service.js
@@ -0,0 +1,570 @@
+/*
+Copyright 2014 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+/*
+This service handles what should happen when you get an event. This service does
+not care where the event came from, it only needs enough context to be able to
+process them. Events may be coming from the event stream, the REST API (via
+direct GETs or via a pagination stream API), etc.
+
+Typically, this service will store events and broadcast them to any listeners
+(e.g. controllers) via $broadcast.
+*/
+angular.module('eventHandlerService', [])
+.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', '$filter', 'mPresence', 'notificationService', 'modelService',
+function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificationService, modelService) {
+ var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
+ var MSG_EVENT = "MSG_EVENT";
+ var MEMBER_EVENT = "MEMBER_EVENT";
+ var PRESENCE_EVENT = "PRESENCE_EVENT";
+ var POWERLEVEL_EVENT = "POWERLEVEL_EVENT";
+ var CALL_EVENT = "CALL_EVENT";
+ var NAME_EVENT = "NAME_EVENT";
+ var TOPIC_EVENT = "TOPIC_EVENT";
+ var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted
+
+ // 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 = {};
+
+ var initialSyncDeferred;
+
+ var reset = function() {
+ initialSyncDeferred = $q.defer();
+
+ eventMap = {};
+ };
+ reset();
+
+ var resetRoomMessages = function(room_id) {
+ var room = modelService.getRoom(room_id);
+ room.events = [];
+ };
+
+ // Generic method to handle events data
+ var handleRoomStateEvent = function(event, isLiveEvent, addToRoomMessages) {
+ var room = modelService.getRoom(event.room_id);
+ if (addToRoomMessages) {
+ // some state events are displayed as messages, so add them.
+ room.addMessageEvent(event, !isLiveEvent);
+ }
+
+ if (isLiveEvent) {
+ // update the current room state with the latest state
+ room.current_room_state.storeStateEvent(event);
+ }
+ else {
+ var eventTs = event.origin_server_ts;
+ var storedEvent = room.current_room_state.getStateEvent(event.type, event.state_key);
+ if (storedEvent) {
+ if (storedEvent.origin_server_ts < eventTs) {
+ // the incoming event is newer, use it.
+ room.current_room_state.storeStateEvent(event);
+ }
+ }
+ }
+ // TODO: handle old_room_state
+ };
+
+ var handleRoomCreate = function(event, isLiveEvent) {
+ $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
+ };
+
+ var handleRoomAliases = function(event, isLiveEvent) {
+ modelService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
+ };
+
+ var containsBingWord = function(event) {
+ if (!event.content || !event.content.body) {
+ return false;
+ }
+
+ return notificationService.containsBingWord(
+ matrixService.config().user_id,
+ matrixService.config().display_name,
+ matrixService.config().bingWords,
+ event.content.body
+ );
+ };
+
+ var displayNotification = function(event) {
+ if (window.Notification && event.user_id != matrixService.config().user_id) {
+ var member = modelService.getMember(event.room_id, event.user_id);
+ var displayname = $filter("mUserDisplayName")(event.user_id, event.room_id);
+ var message;
+ var shouldBing = false;
+
+ if (event.type === "m.room.message") {
+ shouldBing = containsBingWord(event);
+ message = event.content.body;
+ if (event.content.msgtype === "m.emote") {
+ message = "* " + displayname + " " + message;
+ }
+ else if (event.content.msgtype === "m.image") {
+ message = displayname + " sent an image.";
+ }
+ }
+ else if (event.type == "m.room.member") {
+ // Notify when another user joins only
+ if (event.state_key !== matrixService.config().user_id && "join" === event.content.membership) {
+ member = modelService.getMember(event.room_id, event.state_key);
+ displayname = $filter("mUserDisplayName")(event.state_key, event.room_id);
+ message = displayname + " joined";
+ shouldBing = true;
+ }
+ else {
+ return;
+ }
+ }
+
+ // Ideally we would notify only when the window is hidden (i.e. document.hidden = true).
+ //
+ // However, Chrome on Linux and OSX currently returns document.hidden = false unless the window is
+ // explicitly showing a different tab. So we need another metric to determine hiddenness - we
+ // simply use idle time. If the user has been idle enough that their presence goes to idle, then
+ // we also display notifs when things happen.
+ //
+ // This is far far better than notifying whenever anything happens anyway, otherwise you get spammed
+ // to death with notifications when the window is in the foreground, which is horrible UX (especially
+ // if you have not defined any bingers and so get notified for everything).
+ var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState());
+
+ // We need a way to let people get notifications for everything, if they so desire. The way to do this
+ // is to specify zero bingwords.
+ var bingWords = matrixService.config().bingWords;
+ if (bingWords === undefined || bingWords.length === 0) {
+ shouldBing = true;
+ }
+
+ if (shouldBing && isIdle) {
+ console.log("Displaying notification for "+JSON.stringify(event));
+
+ var roomTitle = $filter("mRoomName")(event.room_id);
+
+ notificationService.showNotification(
+ displayname + " (" + roomTitle + ")",
+ message,
+ member ? member.event.content.avatar_url : undefined,
+ function() {
+ console.log("notification.onclick() room=" + event.room_id);
+ $rootScope.goToPage('room/' + event.room_id);
+ }
+ );
+ }
+ }
+ };
+
+ var handleMessage = function(event, isLiveEvent) {
+ // Check for empty event content
+ var hasContent = false;
+ for (var prop in event.content) {
+ hasContent = true;
+ break;
+ }
+ if (!hasContent) {
+ // empty json object is a redacted event, so ignore.
+ return;
+ }
+
+ // =======================
+
+ var room = modelService.getRoom(event.room_id);
+
+ if (event.user_id !== matrixService.config().user_id) {
+ room.addMessageEvent(event, !isLiveEvent);
+ displayNotification(event);
+ }
+ else {
+ // we may have locally echoed this, so we should replace the event
+ // instead of just adding.
+ room.addOrReplaceMessageEvent(event, !isLiveEvent);
+ }
+
+ // TODO send delivery receipt if isLiveEvent
+
+ $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent);
+ };
+
+ var handleRoomMember = function(event, isLiveEvent, isStateEvent) {
+ var room = modelService.getRoom(event.room_id);
+
+ // did something change?
+ var memberChanges = undefined;
+ if (!isStateEvent) {
+ // could be a membership change, display name change, etc.
+ // Find out which one.
+ if ((event.prev_content === undefined && event.content.membership) || (event.prev_content && (event.prev_content.membership !== event.content.membership))) {
+ memberChanges = "membership";
+ }
+ else if (event.prev_content && (event.prev_content.displayname !== event.content.displayname)) {
+ memberChanges = "displayname";
+ }
+ // mark the key which changed
+ event.changedKey = memberChanges;
+ }
+
+
+ // modify state before adding the message so it points to the right thing.
+ // The events are copied to avoid referencing the same event when adding
+ // the message (circular json structures)
+ if (isStateEvent || isLiveEvent) {
+ var newEvent = angular.copy(event);
+ newEvent.cnt = event.content;
+ room.current_room_state.storeStateEvent(newEvent);
+ }
+ else if (!isLiveEvent) {
+ // mutate the old room state
+ var oldEvent = angular.copy(event);
+ oldEvent.cnt = event.content;
+ if (event.prev_content) {
+ // the m.room.member event we are handling is the NEW event. When
+ // we keep going back in time, we want the PREVIOUS value for displaying
+ // names/etc, hence the clobber here.
+ oldEvent.cnt = event.prev_content;
+ }
+
+ if (event.changedKey === "membership" && event.content.membership === "join") {
+ // join has a prev_content but it doesn't contain all the info unlike the join, so use that.
+ oldEvent.cnt = event.content;
+ }
+
+ room.old_room_state.storeStateEvent(oldEvent);
+ }
+
+ // If there was a change we want to display, dump it in the message
+ // list. This has to be done after room state is updated.
+ if (memberChanges) {
+ room.addMessageEvent(event, !isLiveEvent);
+
+ if (memberChanges === "membership" && isLiveEvent) {
+ displayNotification(event);
+ }
+ }
+
+
+
+ $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent);
+ };
+
+ var handlePresence = function(event, isLiveEvent) {
+ // presence is always current, so clobber.
+ modelService.setUser(event);
+ $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
+ };
+
+ var handlePowerLevels = function(event, isLiveEvent) {
+ handleRoomStateEvent(event, isLiveEvent);
+ $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent);
+ };
+
+ var handleRoomName = function(event, isLiveEvent, isStateEvent) {
+ console.log("handleRoomName room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - name: " + event.content.name);
+ handleRoomStateEvent(event, isLiveEvent, !isStateEvent);
+ $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent);
+ };
+
+
+ var handleRoomTopic = function(event, isLiveEvent, isStateEvent) {
+ console.log("handleRoomTopic room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - topic: " + event.content.topic);
+ handleRoomStateEvent(event, isLiveEvent, !isStateEvent);
+ $rootScope.$broadcast(TOPIC_EVENT, event, isLiveEvent);
+ };
+
+ var handleCallEvent = function(event, isLiveEvent) {
+ $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
+ if (event.type === 'm.call.invite') {
+ var room = modelService.getRoom(event.room_id);
+ room.addMessageEvent(event, !isLiveEvent);
+ }
+ };
+
+ var handleRedaction = function(event, isLiveEvent) {
+ if (!isLiveEvent) {
+ // we have nothing to remove, so just ignore it.
+ console.log("Received redacted event: "+JSON.stringify(event));
+ return;
+ }
+
+ // we need to remove something possibly: do we know the redacted
+ // event ID?
+ if (eventMap[event.redacts]) {
+ var room = modelService.getRoom(event.room_id);
+ // remove event from list of messages in this room.
+ var eventList = room.events;
+ for (var i=0; i<eventList.length; i++) {
+ if (eventList[i].event_id === event.redacts) {
+ console.log("Removing event " + event.redacts);
+ eventList.splice(i, 1);
+ break;
+ }
+ }
+
+ console.log("Redacted an event.");
+ }
+ }
+
+ return {
+ ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
+ MSG_EVENT: MSG_EVENT,
+ MEMBER_EVENT: MEMBER_EVENT,
+ PRESENCE_EVENT: PRESENCE_EVENT,
+ POWERLEVEL_EVENT: POWERLEVEL_EVENT,
+ CALL_EVENT: CALL_EVENT,
+ NAME_EVENT: NAME_EVENT,
+ TOPIC_EVENT: TOPIC_EVENT,
+ RESET_EVENT: RESET_EVENT,
+
+ reset: function() {
+ reset();
+ $rootScope.$broadcast(RESET_EVENT);
+ },
+
+ handleEvent: function(event, isLiveEvent, isStateEvent) {
+
+ // Avoid duplicated events
+ // Needed for rooms where initialSync has not been done.
+ // In this case, we do not know where to start pagination. So, it starts from the END
+ // and we can have the same event (ex: joined, invitation) coming from the pagination
+ // AND from the event stream.
+ // FIXME: This workaround should be no more required when /initialSync on a particular room
+ // will be available (as opposite to the global /initialSync done at startup)
+ if (!isStateEvent) { // Do not consider state events
+ if (event.event_id && eventMap[event.event_id]) {
+ console.log("discarding duplicate event: " + JSON.stringify(event, undefined, 4));
+ return;
+ }
+ else {
+ eventMap[event.event_id] = 1;
+ }
+ }
+
+ if (event.type.indexOf('m.call.') === 0) {
+ handleCallEvent(event, isLiveEvent);
+ }
+ else {
+ switch(event.type) {
+ case "m.room.create":
+ handleRoomCreate(event, isLiveEvent);
+ break;
+ case "m.room.aliases":
+ handleRoomAliases(event, isLiveEvent);
+ break;
+ case "m.room.message":
+ handleMessage(event, isLiveEvent);
+ break;
+ case "m.room.member":
+ handleRoomMember(event, isLiveEvent, isStateEvent);
+ break;
+ case "m.presence":
+ handlePresence(event, isLiveEvent);
+ break;
+ case 'm.room.ops_levels':
+ case 'm.room.send_event_level':
+ case 'm.room.add_state_level':
+ case 'm.room.join_rules':
+ case 'm.room.power_levels':
+ handlePowerLevels(event, isLiveEvent);
+ break;
+ case 'm.room.name':
+ handleRoomName(event, isLiveEvent, isStateEvent);
+ break;
+ case 'm.room.topic':
+ handleRoomTopic(event, isLiveEvent, isStateEvent);
+ break;
+ case 'm.room.redaction':
+ handleRedaction(event, isLiveEvent);
+ break;
+ default:
+ // if it is a state event, then just add it in so it
+ // displays on the Room Info screen.
+ if (typeof(event.state_key) === "string") { // incls. 0-len strings
+ if (event.room_id) {
+ handleRoomStateEvent(event, isLiveEvent, false);
+ }
+ }
+ console.log("Unable to handle event type " + event.type);
+ // console.log(JSON.stringify(event, undefined, 4));
+ break;
+ }
+ }
+ },
+
+ // isLiveEvents determines whether notifications should be shown, whether
+ // messages get appended to the start/end of lists, etc.
+ handleEvents: function(events, isLiveEvents, isStateEvents) {
+ 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, dir) {
+ 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
+ var room = modelService.getRoom(room_id);
+ room.old_room_state.pagination_token = messages.end;
+
+ }
+ else {
+ // InitialSync returns messages in chronological order, so invert
+ // it to get most recent > oldest
+ for (var i=events.length - 1; i>=0; i--) {
+ this.handleEvent(events[i], isLiveEvents, isLiveEvents);
+ }
+ // Store where to start pagination
+ var room = modelService.getRoom(room_id);
+ room.old_room_state.pagination_token = messages.start;
+ }
+ },
+
+ handleInitialSyncDone: function(response) {
+ console.log("# handleInitialSyncDone");
+
+ var rooms = response.data.rooms;
+ for (var i = 0; i < rooms.length; ++i) {
+ var room = rooms[i];
+
+ // FIXME: This is ming: the HS should be sending down the m.room.member
+ // event for the invite in .state but it isn't, so fudge it for now.
+ if (room.inviter && room.membership === "invite") {
+ var me = matrixService.config().user_id;
+ var fakeEvent = {
+ event_id: "__FAKE__" + room.room_id,
+ user_id: room.inviter,
+ origin_server_ts: 0,
+ room_id: room.room_id,
+ state_key: me,
+ type: "m.room.member",
+ content: {
+ membership: "invite"
+ }
+ };
+ if (!room.state) {
+ room.state = [];
+ }
+ room.state.push(fakeEvent);
+ console.log("RECV /initialSync invite >> "+room.room_id);
+ }
+
+ var newRoom = modelService.getRoom(room.room_id);
+ newRoom.current_room_state.storeStateEvents(room.state);
+ newRoom.old_room_state.storeStateEvents(room.state);
+
+ // this should be done AFTER storing state events since these
+ // messages may make the old_room_state diverge.
+ if ("messages" in room) {
+ this.handleRoomMessages(room.room_id, room.messages, false);
+ newRoom.current_room_state.pagination_token = room.messages.end;
+ newRoom.old_room_state.pagination_token = room.messages.start;
+ }
+ }
+ var presence = response.data.presence;
+ this.handleEvents(presence, false);
+
+ initialSyncDeferred.resolve(response);
+ },
+
+ // Returns a promise that resolves when the initialSync request has been processed
+ waitForInitialSyncCompletion: function() {
+ return initialSyncDeferred.promise;
+ },
+
+ resetRoomMessages: function(room_id) {
+ resetRoomMessages(room_id);
+ },
+
+ eventContainsBingWord: function(event) {
+ return containsBingWord(event);
+ },
+
+ /**
+ * 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 events = modelService.getRoom(room_id).events;
+ for (var i = events.length - 1; i >= 0; i--) {
+ var message = events[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 = modelService.getRoom(room_id);
+ memberCount = 0;
+ for (var i in room.current_room_state.members) {
+ if (!room.current_room_state.members.hasOwnProperty(i)) continue;
+
+ var member = room.current_room_state.members[i].event;
+
+ if ("join" === member.content.membership) {
+ memberCount = memberCount + 1;
+ }
+ }
+
+ return memberCount;
+ },
+
+ /**
+ * Return the power level of an user in a particular room
+ * @param {String} room_id the room id
+ * @param {String} user_id the user id
+ * @returns {Number} a value between 0 and 10
+ */
+ getUserPowerLevel: function(room_id, user_id) {
+ var powerLevel = 0;
+ var room = modelService.getRoom(room_id).current_room_state;
+ if (room.state("m.room.power_levels")) {
+ if (user_id in room.state("m.room.power_levels").content) {
+ powerLevel = room.state("m.room.power_levels").content[user_id];
+ }
+ else {
+ // Use the room default user power
+ powerLevel = room.state("m.room.power_levels").content["default"];
+ }
+ }
+ return powerLevel;
+ }
+ };
+}]);
diff --git a/webclient/components/matrix/event-stream-service.js b/syweb/webclient/components/matrix/event-stream-service.js
index 05469a3ded..c03f0b953b 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/syweb/webclient/components/matrix/event-stream-service.js
@@ -109,25 +109,6 @@ angular.module('eventStreamService', [])
// 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) {
- var room = rooms[i];
-
- eventHandlerService.initRoom(room);
-
- if ("messages" in room) {
- eventHandlerService.handleRoomMessages(room.room_id, room.messages, false);
- }
-
- if ("state" in room) {
- eventHandlerService.handleEvents(room.state, false, true);
- }
- }
-
- var presence = response.data.presence;
- eventHandlerService.handleEvents(presence, false);
-
- // Initial sync is done
eventHandlerService.handleInitialSyncDone(response);
// Start event streaming from that point
diff --git a/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js
index 3e8811e5fc..56431817d9 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/syweb/webclient/components/matrix/matrix-call.js
@@ -35,19 +35,16 @@ var forAllTracksOnStream = function(s, f) {
forAllAudioTracksOnStream(s, f);
}
-navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
-window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible
-window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
-window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
-
-// Returns true if the browser supports all required features to make WebRTC call
-var isWebRTCSupported = function () {
- return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate);
-};
-
angular.module('MatrixCall', [])
-.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope, $timeout) {
- $rootScope.isWebRTCSupported = isWebRTCSupported();
+.factory('MatrixCall', ['matrixService', 'matrixPhoneService', 'modelService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, modelService, $rootScope, $timeout) {
+ $rootScope.isWebRTCSupported = function () {
+ navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+ window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible
+ window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
+ window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
+
+ return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate);
+ };
var MatrixCall = function(room_id) {
this.room_id = room_id;
@@ -60,7 +57,7 @@ angular.module('MatrixCall', [])
this.candidateSendTries = 0;
var self = this;
- $rootScope.$watch(this.remoteVideoElement, function (oldValue, newValue) {
+ $rootScope.$watch(this.getRemoteVideoElement(), function (oldValue, newValue) {
self.tryPlayRemoteStream();
});
@@ -85,7 +82,7 @@ angular.module('MatrixCall', [])
});
}
- // FIXME: we should prevent any class from being placed or accepted before this has finished
+ // FIXME: we should prevent any calls from being placed or accepted before this has finished
MatrixCall.getTurnServer();
MatrixCall.CALL_TIMEOUT = 60000;
@@ -95,7 +92,8 @@ angular.module('MatrixCall', [])
var pc;
if (window.mozRTCPeerConnection) {
var iceServers = [];
- if (MatrixCall.turnServer) {
+ // https://github.com/EricssonResearch/openwebrtc/issues/85
+ if (MatrixCall.turnServer /*&& !this.isOpenWebRTC()*/) {
if (MatrixCall.turnServer.uris) {
for (var i = 0; i < MatrixCall.turnServer.uris.length; i++) {
iceServers.push({
@@ -113,7 +111,8 @@ angular.module('MatrixCall', [])
pc = new window.mozRTCPeerConnection({"iceServers":iceServers});
} else {
var iceServers = [];
- if (MatrixCall.turnServer) {
+ // https://github.com/EricssonResearch/openwebrtc/issues/85
+ if (MatrixCall.turnServer && !this.isOpenWebRTC()) {
if (MatrixCall.turnServer.uris) {
iceServers.push({
'urls': MatrixCall.turnServer.uris,
@@ -178,7 +177,8 @@ angular.module('MatrixCall', [])
this.state = 'ringing';
this.direction = 'inbound';
- if (window.mozRTCPeerConnection) {
+ // This also applied to the Safari OpenWebRTC extension so let's just do this all the time at least for now
+ //if (window.mozRTCPeerConnection) {
// firefox's RTCPeerConnection doesn't add streams until it starts getting media on them
// so we need to figure out whether a video channel has been offered by ourselves.
if (this.msg.offer.sdp.indexOf('m=video') > -1) {
@@ -186,7 +186,7 @@ angular.module('MatrixCall', [])
} else {
this.type = 'voice';
}
- }
+ //}
var self = this;
$timeout(function() {
@@ -213,8 +213,8 @@ angular.module('MatrixCall', [])
var self = this;
- var roomMembers = $rootScope.events.rooms[this.room_id].members;
- if (roomMembers[matrixService.config().user_id].membership != 'join') {
+ var roomMembers = modelService.getRoom(this.room_id).current_room_state.members;
+ if (roomMembers[matrixService.config().user_id].event.content.membership != 'join') {
console.log("We need to join the room before we can accept this call");
matrixService.join(this.room_id).then(function() {
self.answer();
@@ -254,8 +254,8 @@ angular.module('MatrixCall', [])
// pausing now keeps the last frame (ish) of the video call in the video element
// rather than it just turning black straight away
- if (this.remoteVideoElement) this.remoteVideoElement.pause();
- if (this.localVideoElement) this.localVideoElement.pause();
+ if (this.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause();
+ if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().pause();
this.stopAllMedia();
if (this.peerConn) this.peerConn.close();
@@ -280,11 +280,18 @@ angular.module('MatrixCall', [])
}
if (this.state == 'ended') return;
- if (this.localVideoElement && this.type == 'video') {
+ var videoEl = this.getLocalVideoElement();
+
+ if (videoEl && this.type == 'video') {
var vidTrack = stream.getVideoTracks()[0];
- this.localVideoElement.src = URL.createObjectURL(stream);
- this.localVideoElement.muted = true;
- this.localVideoElement.play();
+ videoEl.autoplay = true;
+ videoEl.src = URL.createObjectURL(stream);
+ videoEl.muted = true;
+ var self = this;
+ $timeout(function() {
+ var vel = self.getLocalVideoElement();
+ if (vel.play) vel.play();
+ });
}
this.localAVStream = stream;
@@ -308,11 +315,18 @@ angular.module('MatrixCall', [])
MatrixCall.prototype.gotUserMediaForAnswer = function(stream) {
if (this.state == 'ended') return;
- if (this.localVideoElement && this.type == 'video') {
+ var localVidEl = this.getLocalVideoElement();
+
+ if (localVidEl && this.type == 'video') {
+ localVidEl.autoplay = true;
var vidTrack = stream.getVideoTracks()[0];
- this.localVideoElement.src = URL.createObjectURL(stream);
- this.localVideoElement.muted = true;
- this.localVideoElement.play();
+ localVidEl.src = URL.createObjectURL(stream);
+ localVidEl.muted = true;
+ var self = this;
+ $timeout(function() {
+ var vel = self.getLocalVideoElement();
+ if (vel.play) vel.play();
+ });
}
this.localAVStream = stream;
@@ -341,11 +355,11 @@ angular.module('MatrixCall', [])
}
MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
- console.log("Got remote ICE "+cand.sdpMid+" candidate: "+cand.candidate);
if (this.state == 'ended') {
- console.log("Ignoring remote ICE candidate because call has ended");
+ //console.log("Ignoring remote ICE candidate because call has ended");
return;
}
+ console.log("Got remote ICE "+cand.sdpMid+" candidate: "+cand.candidate);
this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {});
};
@@ -365,41 +379,46 @@ angular.module('MatrixCall', [])
return;
}
- this.peerConn.setLocalDescription(description);
-
- var content = {
- version: 0,
- call_id: this.call_id,
- offer: description,
- lifetime: MatrixCall.CALL_TIMEOUT
- };
- this.sendEventWithRetry('m.call.invite', content);
-
var self = this;
- $timeout(function() {
- if (self.state == 'invite_sent') {
- self.hangup('invite_timeout');
- }
- }, MatrixCall.CALL_TIMEOUT);
+ this.peerConn.setLocalDescription(description, function() {
+ var content = {
+ version: 0,
+ call_id: self.call_id,
+ // OpenWebRTC appears to add extra stuff (like the DTLS fingerprint) to the description
+ // when setting it on the peerconnection. According to the spec it should only add ICE
+ // candidates. Any ICE candidates that have already been generated at this point will
+ // probably be sent both in the offer and separately. Ho hum.
+ offer: self.peerConn.localDescription,
+ lifetime: MatrixCall.CALL_TIMEOUT
+ };
+ self.sendEventWithRetry('m.call.invite', content);
+
+ $timeout(function() {
+ if (self.state == 'invite_sent') {
+ self.hangup('invite_timeout');
+ }
+ }, MatrixCall.CALL_TIMEOUT);
- $rootScope.$apply(function() {
- self.state = 'invite_sent';
- });
+ $rootScope.$apply(function() {
+ self.state = 'invite_sent';
+ });
+ }, function() { console.log("Error setting local description!"); });
};
MatrixCall.prototype.createdAnswer = function(description) {
console.log("Created answer: "+description);
- this.peerConn.setLocalDescription(description);
- var content = {
- version: 0,
- call_id: this.call_id,
- answer: description
- };
- this.sendEventWithRetry('m.call.answer', content);
var self = this;
- $rootScope.$apply(function() {
- self.state = 'connecting';
- });
+ this.peerConn.setLocalDescription(description, function() {
+ var content = {
+ version: 0,
+ call_id: self.call_id,
+ answer: self.peerConn.localDescription
+ };
+ self.sendEventWithRetry('m.call.answer', content);
+ $rootScope.$apply(function() {
+ self.state = 'connecting';
+ });
+ }, function() { console.log("Error setting local description!"); } );
};
MatrixCall.prototype.getLocalOfferFailed = function(error) {
@@ -467,10 +486,17 @@ angular.module('MatrixCall', [])
};
MatrixCall.prototype.tryPlayRemoteStream = function(event) {
- if (this.remoteVideoElement && this.remoteAVStream) {
- var player = this.remoteVideoElement;
+ if (this.getRemoteVideoElement() && this.remoteAVStream) {
+ var player = this.getRemoteVideoElement();
+ player.autoplay = true;
player.src = URL.createObjectURL(this.remoteAVStream);
- player.play();
+ var self = this;
+ $timeout(function() {
+ var vel = self.getRemoteVideoElement();
+ if (vel.play) vel.play();
+ // OpenWebRTC does not support oniceconnectionstatechange yet
+ if (self.isOpenWebRTC()) self.state = 'connected';
+ });
}
};
@@ -502,8 +528,8 @@ angular.module('MatrixCall', [])
MatrixCall.prototype.onHangupReceived = function(msg) {
console.log("Hangup received");
- if (this.remoteVideoElement) this.remoteVideoElement.pause();
- if (this.localVideoElement) this.localVideoElement.pause();
+ if (this.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause();
+ if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().pause();
this.state = 'ended';
this.hangupParty = 'remote';
this.hangupReason = msg.reason;
@@ -526,8 +552,8 @@ angular.module('MatrixCall', [])
newCall.gotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
}
- newCall.localVideoElement = this.localVideoElement;
- newCall.remoteVideoElement = this.remoteVideoElement;
+ newCall.localVideoSelector = this.localVideoSelector;
+ newCall.remoteVideoSelector = this.remoteVideoSelector;
this.successor = newCall;
this.hangup(true);
};
@@ -603,5 +629,31 @@ angular.module('MatrixCall', [])
}, delayMs);
};
+ MatrixCall.prototype.getLocalVideoElement = function() {
+ if (this.localVideoSelector) {
+ var t = angular.element(this.localVideoSelector);
+ if (t.length) return t[0];
+ }
+ return null;
+ };
+
+ MatrixCall.prototype.getRemoteVideoElement = function() {
+ if (this.remoteVideoSelector) {
+ var t = angular.element(this.remoteVideoSelector);
+ if (t.length) return t[0];
+ }
+ return null;
+ };
+
+ MatrixCall.prototype.isOpenWebRTC = function() {
+ var scripts = angular.element('script');
+ for (var i = 0; i < scripts.length; i++) {
+ if (scripts[i].src.indexOf("owr.js") > -1) {
+ return true;
+ }
+ }
+ return false;
+ };
+
return MatrixCall;
}]);
diff --git a/syweb/webclient/components/matrix/matrix-filter.js b/syweb/webclient/components/matrix/matrix-filter.js
new file mode 100644
index 0000000000..cef9235891
--- /dev/null
+++ b/syweb/webclient/components/matrix/matrix-filter.js
@@ -0,0 +1,172 @@
+/*
+ Copyright 2014 OpenMarket Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+'use strict';
+
+angular.module('matrixFilter', [])
+
+// Compute the room name according to information we have
+// TODO: It would be nice if this was stateless and had no dependencies. That would
+// make the business logic here a lot easier to see.
+.filter('mRoomName', ['$rootScope', 'matrixService', 'modelService', 'mUserDisplayNameFilter',
+function($rootScope, matrixService, modelService, mUserDisplayNameFilter) {
+ return function(room_id) {
+ var roomName;
+
+ // If there is an alias, use it
+ // TODO: only one alias is managed for now
+ var alias = modelService.getRoomIdToAliasMapping(room_id);
+ var room = modelService.getRoom(room_id).current_room_state;
+
+ var room_name_event = room.state("m.room.name");
+
+ // Determine if it is a public room
+ var isPublicRoom = false;
+ if (room.state("m.room.join_rules") && room.state("m.room.join_rules").content) {
+ isPublicRoom = ("public" === room.state("m.room.join_rules").content.join_rule);
+ }
+
+ if (room_name_event) {
+ roomName = room_name_event.content.name;
+ }
+ else if (alias) {
+ roomName = alias;
+ }
+ else if (Object.keys(room.members).length > 0 && !isPublicRoom) { // Do not rename public room
+ var user_id = matrixService.config().user_id;
+
+ // this is a "one to one" room and should have the name of the other user.
+ if (Object.keys(room.members).length === 2) {
+ for (var i in room.members) {
+ if (!room.members.hasOwnProperty(i)) continue;
+
+ var member = room.members[i].event;
+ if (member.state_key !== user_id) {
+ roomName = mUserDisplayNameFilter(member.state_key, room_id);
+ if (!roomName) {
+ roomName = member.state_key;
+ }
+ break;
+ }
+ }
+ }
+ else if (Object.keys(room.members).length === 1) {
+ // this could be just us (self-chat) or could be the other person
+ // in a room if they have invited us to the room. Find out which.
+ var otherUserId = Object.keys(room.members)[0];
+ if (otherUserId === user_id) {
+ // it's us, we may have been invited to this room or it could
+ // be a self chat.
+ if (room.members[otherUserId].event.content.membership === "invite") {
+ // someone invited us, use the right ID.
+ roomName = mUserDisplayNameFilter(room.members[otherUserId].event.user_id, room_id);
+ if (!roomName) {
+ roomName = room.members[otherUserId].event.user_id;
+ }
+ }
+ else {
+ roomName = mUserDisplayNameFilter(otherUserId, room_id);
+ if (!roomName) {
+ roomName = user_id;
+ }
+ }
+ }
+ else { // it isn't us, so use their name if we know it.
+ roomName = mUserDisplayNameFilter(otherUserId, room_id);
+ if (!roomName) {
+ roomName = otherUserId;
+ }
+ }
+ }
+ else if (Object.keys(room.members).length === 0) {
+ // this shouldn't be possible
+ console.error("0 members in room >> " + room_id);
+ }
+ }
+
+
+ // 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;
+ }
+
+ return roomName;
+ };
+}])
+
+// Return the user display name
+.filter('mUserDisplayName', ['modelService', 'matrixService', function(modelService, matrixService) {
+ /**
+ * Return the display name of an user acccording to data already downloaded
+ * @param {String} user_id the id of the user
+ * @param {String} room_id the room id
+ * @param {boolean} wrap whether to insert whitespace into the userid (if displayname not available) to help it wrap
+ * @returns {String} A suitable display name for the user.
+ */
+ return function(user_id, room_id, wrap) {
+ var displayName;
+
+ // Get the user display name from the member list of the room
+ var member = modelService.getMember(room_id, user_id);
+ if (member) {
+ member = member.event;
+ }
+ if (member && member.content.displayname) { // Do not consider null displayname
+ displayName = member.content.displayname;
+
+ // Disambiguate users who have the same displayname in the room
+ if (user_id !== matrixService.config().user_id) {
+ var room = modelService.getRoom(room_id);
+
+ for (var member_id in room.current_room_state.members) {
+ if (room.current_room_state.members.hasOwnProperty(member_id) && member_id !== user_id) {
+ var member2 = room.current_room_state.members[member_id].event;
+ if (member2.content.displayname && member2.content.displayname === displayName) {
+ displayName = displayName + " (" + user_id + ")";
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // The user may not have joined the room yet. So try to resolve display name from presence data
+ // Note: This data may not be available
+ if (undefined === displayName) {
+ var usr = modelService.getUser(user_id);
+ if (usr) {
+ displayName = usr.event.content.displayname;
+ }
+ }
+
+ if (undefined === displayName) {
+ // By default, use the user ID
+ if (wrap && user_id.indexOf(':') >= 0) {
+ displayName = user_id.substr(0, user_id.indexOf(':')) + " " + user_id.substr(user_id.indexOf(':'));
+ }
+ else {
+ displayName = user_id;
+ }
+ }
+
+ return displayName;
+ };
+}]);
diff --git a/webclient/components/matrix/matrix-phone-service.js b/syweb/webclient/components/matrix/matrix-phone-service.js
index 06465ed821..55dbbf522e 100644
--- a/webclient/components/matrix/matrix-phone-service.js
+++ b/syweb/webclient/components/matrix/matrix-phone-service.js
@@ -60,7 +60,7 @@ angular.module('matrixPhoneService', [])
var MatrixCall = $injector.get('MatrixCall');
var call = new MatrixCall(event.room_id);
- if (!isWebRTCSupported()) {
+ if (!$rootScope.isWebRTCSupported()) {
console.log("Incoming call ID "+msg.call_id+" but this browser doesn't support WebRTC");
// don't hang up the call: there could be other clients connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
diff --git a/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js
index 1840cf46c0..cfe8691f85 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/syweb/webclient/components/matrix/matrix-service.js
@@ -23,7 +23,7 @@ This serves to isolate the caller from changes to the underlying url paths, as
well as attach common params (e.g. access_token) to requests.
*/
angular.module('matrixService', [])
-.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) {
+.factory('matrixService', ['$http', '$q', function($http, $q) {
/*
* Permanent storage of user information
@@ -36,13 +36,9 @@ angular.module('matrixService', [])
*/
var config;
- var roomIdToAlias = {};
- var aliasToRoomId = {};
-
// Current version of permanent storage
var configVersion = 0;
var prefixPath = "/_matrix/client/api/v1";
- var MAPPING_PREFIX = "alias_for_";
var doRequest = function(method, path, params, data, $httpParams) {
if (!config) {
@@ -267,7 +263,7 @@ angular.module('matrixService', [])
// get room state for a specific room
roomState: function(room_id) {
- var path = "/rooms/" + room_id + "/state";
+ var path = "/rooms/" + encodeURIComponent(room_id) + "/state";
return doRequest("GET", path);
},
@@ -375,9 +371,11 @@ angular.module('matrixService', [])
sendStateEvent: function(room_id, eventType, content, state_key) {
- var path = "/rooms/$room_id/state/"+eventType;
+ var path = "/rooms/$room_id/state/"+ eventType;
+ // TODO: uncomment this when matrix.org is updated, else all state events 500.
+ // var path = "/rooms/$room_id/state/"+ encodeURIComponent(eventType);
if (state_key !== undefined) {
- path += "/" + state_key;
+ path += "/" + encodeURIComponent(state_key);
}
room_id = encodeURIComponent(room_id);
path = path.replace("$room_id", room_id);
@@ -422,7 +420,8 @@ angular.module('matrixService', [])
var content = {
msgtype: "m.image",
url: image_url,
- body: image_body
+ info: image_body,
+ body: "Image"
};
return this.sendMessage(room_id, msg_id, content);
@@ -440,7 +439,8 @@ angular.module('matrixService', [])
redactEvent: function(room_id, event_id) {
var path = "/rooms/$room_id/redact/$event_id";
- path = path.replace("$room_id", room_id);
+ path = path.replace("$room_id", encodeURIComponent(room_id));
+ // TODO: encodeURIComponent when HS updated.
path = path.replace("$event_id", event_id);
var content = {};
return doRequest("POST", path, undefined, content);
@@ -458,7 +458,7 @@ angular.module('matrixService', [])
paginateBackMessages: function(room_id, from_token, limit) {
var path = "/rooms/$room_id/messages";
- path = path.replace("$room_id", room_id);
+ path = path.replace("$room_id", encodeURIComponent(room_id));
var params = {
from: from_token,
limit: limit,
@@ -506,12 +506,12 @@ angular.module('matrixService', [])
setProfileInfo: function(data, info_segment) {
var path = "/profile/$user/" + info_segment;
- path = path.replace("$user", config.user_id);
+ path = path.replace("$user", encodeURIComponent(config.user_id));
return doRequest("PUT", path, undefined, data);
},
getProfileInfo: function(userId, info_segment) {
- var path = "/profile/"+userId
+ var path = "/profile/"+encodeURIComponent(userId);
if (info_segment) path += '/' + info_segment;
return doRequest("GET", path);
},
@@ -630,7 +630,7 @@ angular.module('matrixService', [])
// Set the logged in user presence state
setUserPresence: function(presence) {
var path = "/presence/$user_id/status";
- path = path.replace("$user_id", config.user_id);
+ path = path.replace("$user_id", encodeURIComponent(config.user_id));
return doRequest("PUT", path, undefined, {
presence: presence
});
@@ -667,114 +667,30 @@ angular.module('matrixService', [])
config.version = configVersion;
localStorage.setItem("config", JSON.stringify(config));
},
-
-
- /****** Room aliases management ******/
-
- /**
- * Get the room_alias & room_display_name which are computed from data
- * already retrieved from the server.
- * @param {Room object} room one element of the array returned by the response
- * of rooms() and publicRooms()
- * @returns {Object} {room_alias: "...", room_display_name: "..."}
- */
- getRoomAliasAndDisplayName: function(room) {
- var result = {
- room_alias: undefined,
- room_display_name: undefined
- };
- var alias = this.getRoomIdToAliasMapping(room.room_id);
- if (alias) {
- // use the existing alias from storage
- result.room_alias = alias;
- result.room_display_name = alias;
- }
- // XXX: this only lets us learn aliases from our local HS - we should
- // make the client stop returning this if we can trust m.room.aliases state events
- else if (room.aliases && room.aliases[0]) {
- // save the mapping
- // TODO: select the smarter alias from the array
- this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]);
- result.room_display_name = room.aliases[0];
- result.room_alias = room.aliases[0];
- }
- else if (room.membership === "invite" && "inviter" in room) {
- result.room_display_name = room.inviter + "'s room";
- }
- else {
- // last resort use the room id
- result.room_display_name = room.room_id;
- }
- return result;
- },
-
- createRoomIdToAliasMapping: function(roomId, alias) {
- roomIdToAlias[roomId] = alias;
- aliasToRoomId[alias] = roomId;
- },
-
- getRoomIdToAliasMapping: function(roomId) {
- var alias = roomIdToAlias[roomId];
- //console.log("looking for alias for " + roomId + "; found: " + alias);
- return alias;
- },
-
- getAliasToRoomIdMapping: function(alias) {
- var roomId = aliasToRoomId[alias];
- //console.log("looking for roomId for " + alias + "; found: " + roomId);
- return roomId;
- },
-
- /****** Power levels management ******/
-
- /**
- * Return the power level of an user in a particular room
- * @param {String} room_id the room id
- * @param {String} user_id the user id
- * @returns {Number} a value between 0 and 10
- */
- getUserPowerLevel: function(room_id, user_id) {
- var powerLevel = 0;
- var room = $rootScope.events.rooms[room_id];
- if (room && room["m.room.power_levels"]) {
- if (user_id in room["m.room.power_levels"].content) {
- powerLevel = room["m.room.power_levels"].content[user_id];
- }
- else {
- // Use the room default user power
- powerLevel = room["m.room.power_levels"].content["default"];
- }
- }
- return powerLevel;
- },
/**
* Change or reset the power level of a user
* @param {String} room_id the room id
* @param {String} user_id the user id
- * @param {Number} powerLevel a value between 0 and 10
+ * @param {Number} powerLevel The desired power level.
* If undefined, the user power level will be reset, ie he will use the default room user power level
+ * @param event The existing m.room.power_levels event if one exists.
* @returns {promise} an $http promise
*/
- setUserPowerLevel: function(room_id, user_id, powerLevel) {
-
- // Hack: currently, there is no home server API so do it by hand by updating
- // the current m.room.power_levels of the room and send it to the server
- var room = $rootScope.events.rooms[room_id];
- if (room && room["m.room.power_levels"]) {
- var content = angular.copy(room["m.room.power_levels"].content);
- content[user_id] = powerLevel;
+ setUserPowerLevel: function(room_id, user_id, powerLevel, event) {
+ var content = {};
+ if (event) {
+ // if there is an existing event, copy the content as it contains
+ // the power level values for other members which we do not want
+ // to modify.
+ content = angular.copy(event.content);
+ }
+ content[user_id] = powerLevel;
- var path = "/rooms/$room_id/state/m.room.power_levels";
- path = path.replace("$room_id", encodeURIComponent(room_id));
+ var path = "/rooms/$room_id/state/m.room.power_levels";
+ path = path.replace("$room_id", encodeURIComponent(room_id));
- return doRequest("PUT", path, undefined, content);
- }
-
- // The room does not exist or does not contain power_levels data
- var deferred = $q.defer();
- deferred.reject({data:{error: "Invalid room: " + room_id}});
- return deferred.promise;
+ return doRequest("PUT", path, undefined, content);
},
getTurnServer: function() {
diff --git a/syweb/webclient/components/matrix/model-service.js b/syweb/webclient/components/matrix/model-service.js
new file mode 100644
index 0000000000..da71dac436
--- /dev/null
+++ b/syweb/webclient/components/matrix/model-service.js
@@ -0,0 +1,213 @@
+/*
+Copyright 2014 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+/*
+This service serves as the entry point for all models in the app. If access to
+underlying data in a room is required, then this service should be used as the
+dependency.
+*/
+// NB: This is more explicit than linking top-level models to $rootScope
+// in that by adding this service as a dep you are clearly saying "this X
+// needs access to the underlying data store", rather than polluting the
+// $rootScope.
+angular.module('modelService', [])
+.factory('modelService', ['matrixService', function(matrixService) {
+
+ // alias / id lookups
+ var roomIdToAlias = {};
+ var aliasToRoomId = {};
+ var setRoomIdToAliasMapping = function(roomId, alias) {
+ roomIdToAlias[roomId] = alias;
+ aliasToRoomId[alias] = roomId;
+ };
+
+ /***** Room Object *****/
+ var Room = function Room(room_id) {
+ this.room_id = room_id;
+ this.old_room_state = new RoomState();
+ this.current_room_state = new RoomState();
+ this.events = []; // events which can be displayed on the UI. TODO move?
+ };
+ Room.prototype = {
+ addMessageEvents: function addMessageEvents(events, toFront) {
+ for (var i=0; i<events.length; i++) {
+ this.addMessageEvent(events[i], toFront);
+ }
+ },
+
+ addMessageEvent: function addMessageEvent(event, toFront) {
+ // every message must reference the RoomMember which made it *at
+ // that time* so things like display names display correctly.
+ var stateAtTheTime = toFront ? this.old_room_state : this.current_room_state;
+ event.__room_member = stateAtTheTime.getStateEvent("m.room.member", event.user_id);
+ if (event.type === "m.room.member" && event.content.membership === "invite") {
+ // give information on both the inviter and invitee
+ event.__target_room_member = stateAtTheTime.getStateEvent("m.room.member", event.state_key);
+ }
+
+ if (toFront) {
+ this.events.unshift(event);
+ }
+ else {
+ this.events.push(event);
+ }
+ },
+
+ addOrReplaceMessageEvent: function addOrReplaceMessageEvent(event, toFront) {
+ // Start looking from the tail since the first goal of this function
+ // is to find a message among the latest ones
+ for (var i = this.events.length - 1; i >= 0; i--) {
+ var storedEvent = this.events[i];
+ if (storedEvent.event_id === event.event_id) {
+ // It's clobbering time!
+ this.events[i] = event;
+ return;
+ }
+ }
+ this.addMessageEvent(event, toFront);
+ },
+
+ leave: function leave() {
+ return matrixService.leave(this.room_id);
+ }
+ };
+
+ /***** Room State Object *****/
+ var RoomState = function RoomState() {
+ // list of RoomMember
+ this.members = {};
+ // state events, the key is a compound of event type + state_key
+ this.state_events = {};
+ this.pagination_token = "";
+ };
+ RoomState.prototype = {
+ // get a state event for this room from this.state_events. State events
+ // are unique per type+state_key tuple, with a lot of events using 0-len
+ // state keys. To make it not Really Annoying to access, this method is
+ // provided which can just be given the type and it will return the
+ // 0-len event by default.
+ state: function state(type, state_key) {
+ if (!type) {
+ return undefined; // event type MUST be specified
+ }
+ if (!state_key) {
+ return this.state_events[type]; // treat as 0-len state key
+ }
+ return this.state_events[type + state_key];
+ },
+
+ storeStateEvent: function storeState(event) {
+ this.state_events[event.type + event.state_key] = event;
+ if (event.type === "m.room.member") {
+ var rm = new RoomMember();
+ rm.event = event;
+ this.members[event.state_key] = rm;
+ }
+ else if (event.type === "m.room.aliases") {
+ setRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
+ }
+ },
+
+ storeStateEvents: function storeState(events) {
+ if (!events) {
+ return;
+ }
+ for (var i=0; i<events.length; i++) {
+ this.storeStateEvent(events[i]);
+ }
+ },
+
+ getStateEvent: function getStateEvent(event_type, state_key) {
+ return this.state_events[event_type + state_key];
+ }
+ };
+
+ /***** Room Member Object *****/
+ var RoomMember = function RoomMember() {
+ this.event = {}; // the m.room.member event representing the RoomMember.
+ this.user = undefined; // the User
+ };
+
+ /***** User Object *****/
+ var User = function User() {
+ this.event = {}; // the m.presence event representing the User.
+ };
+
+ // rooms are stored here when they come in.
+ var rooms = {
+ // roomid: <Room>
+ };
+
+ var users = {
+ // user_id: <User>
+ };
+
+ console.log("Models inited.");
+
+ return {
+
+ getRoom: function(roomId) {
+ if(!rooms[roomId]) {
+ rooms[roomId] = new Room(roomId);
+ }
+ return rooms[roomId];
+ },
+
+ getRooms: function() {
+ return rooms;
+ },
+
+ /**
+ * 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 room = this.getRoom(room_id);
+ return room.current_room_state.members[user_id];
+ },
+
+ createRoomIdToAliasMapping: function(roomId, alias) {
+ setRoomIdToAliasMapping(roomId, alias);
+ },
+
+ getRoomIdToAliasMapping: function(roomId) {
+ var alias = roomIdToAlias[roomId];
+ //console.log("looking for alias for " + roomId + "; found: " + alias);
+ return alias;
+ },
+
+ getAliasToRoomIdMapping: function(alias) {
+ var roomId = aliasToRoomId[alias];
+ //console.log("looking for roomId for " + alias + "; found: " + roomId);
+ return roomId;
+ },
+
+ getUser: function(user_id) {
+ return users[user_id];
+ },
+
+ setUser: function(event) {
+ var usr = new User();
+ usr.event = event;
+ users[event.content.user_id] = usr;
+ }
+
+ };
+}]);
diff --git a/syweb/webclient/components/matrix/notification-service.js b/syweb/webclient/components/matrix/notification-service.js
new file mode 100644
index 0000000000..9a911413c3
--- /dev/null
+++ b/syweb/webclient/components/matrix/notification-service.js
@@ -0,0 +1,104 @@
+/*
+Copyright 2014 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+/*
+This service manages notifications: enabling, creating and showing them. This
+also contains 'bing word' logic.
+*/
+angular.module('notificationService', [])
+.factory('notificationService', ['$timeout', function($timeout) {
+
+ var getLocalPartFromUserId = function(user_id) {
+ if (!user_id) {
+ return null;
+ }
+ var localpartRegex = /@(.*):\w+/i
+ var results = localpartRegex.exec(user_id);
+ if (results && results.length == 2) {
+ return results[1];
+ }
+ return null;
+ };
+
+ return {
+
+ containsBingWord: function(userId, displayName, bingWords, content) {
+ // case-insensitive name check for user_id OR display_name if they exist
+ var userRegex = "";
+ if (userId) {
+ var localpart = getLocalPartFromUserId(userId);
+ if (localpart) {
+ localpart = localpart.toLocaleLowerCase();
+ userRegex += "\\b" + localpart + "\\b";
+ }
+ }
+ if (displayName) {
+ displayName = displayName.toLocaleLowerCase();
+ if (userRegex.length > 0) {
+ userRegex += "|";
+ }
+ userRegex += "\\b" + displayName + "\\b";
+ }
+
+ var regexList = [new RegExp(userRegex, 'i')];
+
+ // bing word list check
+ if (bingWords && bingWords.length > 0) {
+ for (var i=0; i<bingWords.length; i++) {
+ var re = RegExp(bingWords[i], 'i');
+ regexList.push(re);
+ }
+ }
+ return this.hasMatch(regexList, content);
+ },
+
+ hasMatch: function(regExps, content) {
+ if (!content || $.type(content) != "string") {
+ return false;
+ }
+
+ if (regExps && regExps.length > 0) {
+ for (var i=0; i<regExps.length; i++) {
+ if (content.search(regExps[i]) != -1) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+ showNotification: function(title, body, icon, onclick) {
+ var notification = new window.Notification(
+ title,
+ {
+ "body": body,
+ "icon": icon
+ }
+ );
+
+ if (onclick) {
+ notification.onclick = onclick;
+ }
+
+ $timeout(function() {
+ notification.close();
+ }, 5 * 1000);
+ }
+ };
+
+}]);
diff --git a/webclient/components/matrix/presence-service.js b/syweb/webclient/components/matrix/presence-service.js
index b487e3d3bd..b487e3d3bd 100644
--- a/webclient/components/matrix/presence-service.js
+++ b/syweb/webclient/components/matrix/presence-service.js
diff --git a/syweb/webclient/components/matrix/recents-service.js b/syweb/webclient/components/matrix/recents-service.js
new file mode 100644
index 0000000000..3d82b8218b
--- /dev/null
+++ b/syweb/webclient/components/matrix/recents-service.js
@@ -0,0 +1,99 @@
+/*
+Copyright 2014 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+/*
+This service manages shared state between *instances* of recent lists. The
+recents controller will hook into this central service to get things like:
+- which rooms should be highlighted
+- which rooms have been binged
+- which room is currently selected
+- etc.
+This is preferable to polluting the $rootScope with recents specific info, and
+makes the dependency on this shared state *explicit*.
+*/
+angular.module('recentsService', [])
+.factory('recentsService', ['$rootScope', 'eventHandlerService', function($rootScope, eventHandlerService) {
+ // notify listeners when variables in the service are updated. We need to do
+ // this since we do not tie them to any scope.
+ var BROADCAST_SELECTED_ROOM_ID = "recentsService:BROADCAST_SELECTED_ROOM_ID(room_id)";
+ var selectedRoomId = undefined;
+
+ var BROADCAST_UNREAD_MESSAGES = "recentsService:BROADCAST_UNREAD_MESSAGES(room_id, unreadCount)";
+ var unreadMessages = {
+ // room_id: <number>
+ };
+
+ var BROADCAST_UNREAD_BING_MESSAGES = "recentsService:BROADCAST_UNREAD_BING_MESSAGES(room_id, event)";
+ var unreadBingMessages = {
+ // room_id: bingEvent
+ };
+
+ // listen for new unread messages
+ $rootScope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
+ if (isLive && event.room_id !== selectedRoomId) {
+ if (eventHandlerService.eventContainsBingWord(event)) {
+ if (!unreadBingMessages[event.room_id]) {
+ unreadBingMessages[event.room_id] = {};
+ }
+ unreadBingMessages[event.room_id] = event;
+ $rootScope.$broadcast(BROADCAST_UNREAD_BING_MESSAGES, event.room_id, event);
+ }
+
+ if (!unreadMessages[event.room_id]) {
+ unreadMessages[event.room_id] = 0;
+ }
+ unreadMessages[event.room_id] += 1;
+ $rootScope.$broadcast(BROADCAST_UNREAD_MESSAGES, event.room_id, unreadMessages[event.room_id]);
+ }
+ });
+
+ return {
+ BROADCAST_SELECTED_ROOM_ID: BROADCAST_SELECTED_ROOM_ID,
+ BROADCAST_UNREAD_MESSAGES: BROADCAST_UNREAD_MESSAGES,
+
+ getSelectedRoomId: function() {
+ return selectedRoomId;
+ },
+
+ setSelectedRoomId: function(room_id) {
+ selectedRoomId = room_id;
+ $rootScope.$broadcast(BROADCAST_SELECTED_ROOM_ID, room_id);
+ },
+
+ getUnreadMessages: function() {
+ return unreadMessages;
+ },
+
+ getUnreadBingMessages: function() {
+ return unreadBingMessages;
+ },
+
+ markAsRead: function(room_id) {
+ if (unreadMessages[room_id]) {
+ unreadMessages[room_id] = 0;
+ }
+ if (unreadBingMessages[room_id]) {
+ unreadBingMessages[room_id] = undefined;
+ }
+ $rootScope.$broadcast(BROADCAST_UNREAD_MESSAGES, room_id, 0);
+ $rootScope.$broadcast(BROADCAST_UNREAD_BING_MESSAGES, room_id, undefined);
+ }
+
+ };
+
+}]);
diff --git a/webclient/components/utilities/utilities-service.js b/syweb/webclient/components/utilities/utilities-service.js
index b417cc5b39..b417cc5b39 100644
--- a/webclient/components/utilities/utilities-service.js
+++ b/syweb/webclient/components/utilities/utilities-service.js
diff --git a/webclient/favicon.ico b/syweb/webclient/favicon.ico
index ba193fabc8..ba193fabc8 100644
--- a/webclient/favicon.ico
+++ b/syweb/webclient/favicon.ico
Binary files differdiff --git a/webclient/home/home-controller.js b/syweb/webclient/home/home-controller.js
index f1295560ef..a9538a0309 100644
--- a/webclient/home/home-controller.js
+++ b/syweb/webclient/home/home-controller.js
@@ -17,8 +17,8 @@ limitations under the License.
'use strict';
angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
-.controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService',
- function($scope, $location, matrixService, eventHandlerService) {
+.controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService', 'modelService', 'recentsService',
+ function($scope, $location, matrixService, eventHandlerService, modelService, recentsService) {
$scope.config = matrixService.config();
$scope.public_rooms = [];
@@ -46,6 +46,8 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
$scope.newChat = {
user: ""
};
+
+ recentsService.setSelectedRoomId(undefined);
var refresh = function() {
@@ -54,11 +56,17 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
$scope.public_rooms = response.data.chunk;
for (var i = 0; i < $scope.public_rooms.length; i++) {
var room = $scope.public_rooms[i];
-
- // Add room_alias & room_display_name members
- angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
- eventHandlerService.setRoomVisibility(room.room_id, "public");
+ if (room.aliases && room.aliases.length > 0) {
+ room.room_display_name = room.aliases[0];
+ room.room_alias = room.aliases[0];
+ }
+ else if (room.name) {
+ room.room_display_name = room.name;
+ }
+ else {
+ room.room_display_name = room.room_id;
+ }
}
}
);
@@ -76,7 +84,7 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
// This room has been created. Refresh the rooms list
console.log("Created room " + response.data.room_alias + " with id: "+
response.data.room_id);
- matrixService.createRoomIdToAliasMapping(
+ modelService.createRoomIdToAliasMapping(
response.data.room_id, response.data.room_alias);
},
function(error) {
diff --git a/webclient/home/home.html b/syweb/webclient/home/home.html
index 0af382916e..0af382916e 100644
--- a/webclient/home/home.html
+++ b/syweb/webclient/home/home.html
diff --git a/syweb/webclient/img/attach.png b/syweb/webclient/img/attach.png
new file mode 100644
index 0000000000..d95eabaf00
--- /dev/null
+++ b/syweb/webclient/img/attach.png
Binary files differdiff --git a/webclient/img/close.png b/syweb/webclient/img/close.png
index fbcdb51e6b..fbcdb51e6b 100644
--- a/webclient/img/close.png
+++ b/syweb/webclient/img/close.png
Binary files differdiff --git a/webclient/img/default-profile.png b/syweb/webclient/img/default-profile.png
index 6f81a3c417..6f81a3c417 100644
--- a/webclient/img/default-profile.png
+++ b/syweb/webclient/img/default-profile.png
Binary files differdiff --git a/webclient/img/gradient.png b/syweb/webclient/img/gradient.png
index 8ac9e2193f..8ac9e2193f 100644
--- a/webclient/img/gradient.png
+++ b/syweb/webclient/img/gradient.png
Binary files differdiff --git a/webclient/img/green_phone.png b/syweb/webclient/img/green_phone.png
index 28807c749b..28807c749b 100644
--- a/webclient/img/green_phone.png
+++ b/syweb/webclient/img/green_phone.png
Binary files differdiff --git a/webclient/img/logo-small.png b/syweb/webclient/img/logo-small.png
index 411206dcdc..411206dcdc 100644
--- a/webclient/img/logo-small.png
+++ b/syweb/webclient/img/logo-small.png
Binary files differdiff --git a/webclient/img/logo.png b/syweb/webclient/img/logo.png
index c4b53a8487..c4b53a8487 100644
--- a/webclient/img/logo.png
+++ b/syweb/webclient/img/logo.png
Binary files differdiff --git a/syweb/webclient/img/settings.png b/syweb/webclient/img/settings.png
new file mode 100644
index 0000000000..ac99fe402b
--- /dev/null
+++ b/syweb/webclient/img/settings.png
Binary files differdiff --git a/syweb/webclient/img/video.png b/syweb/webclient/img/video.png
new file mode 100644
index 0000000000..e90afea0c1
--- /dev/null
+++ b/syweb/webclient/img/video.png
Binary files differdiff --git a/syweb/webclient/img/voice.png b/syweb/webclient/img/voice.png
new file mode 100644
index 0000000000..fe464999c0
--- /dev/null
+++ b/syweb/webclient/img/voice.png
Binary files differdiff --git a/webclient/index.html b/syweb/webclient/index.html
index 35c8051298..d9c67333af 100644
--- a/webclient/index.html
+++ b/syweb/webclient/index.html
@@ -13,13 +13,15 @@
<script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
<script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
- <script src="js/angular.min.js"></script>
+ <script src="js/angular.js"></script>
<script src="js/angular-route.min.js"></script>
<script src="js/angular-sanitize.min.js"></script>
- <script src="js/angular-animate.min.js"></script>
+ <script src="js/jquery.peity.min.js"></script>
+ <script src="js/angular-peity.js"></script>
<script type='text/javascript' src="js/ui-bootstrap-tpls-0.11.2.js"></script>
<script type='text/javascript' src='js/ng-infinite-scroll-matrix.js'></script>
<script type='text/javascript' src='js/autofill-event.js'></script>
+ <script type='text/javascript' src='js/elastic.js'></script>
<script src="app.js"></script>
<script src="config.js"></script>
<script src="app-controller.js"></script>
@@ -40,6 +42,10 @@
<script src="components/matrix/matrix-phone-service.js"></script>
<script src="components/matrix/event-stream-service.js"></script>
<script src="components/matrix/event-handler-service.js"></script>
+ <script src="components/matrix/notification-service.js"></script>
+ <script src="components/matrix/recents-service.js"></script>
+ <script src="components/matrix/commands-service.js"></script>
+ <script src="components/matrix/model-service.js"></script>
<script src="components/matrix/presence-service.js"></script>
<script src="components/fileInput/file-input-directive.js"></script>
<script src="components/fileUpload/file-upload-service.js"></script>
@@ -50,8 +56,8 @@
<div id="videoBackground" ng-class="videoMode">
<div id="videoContainer" ng-class="videoMode">
<div id="videoContainerPadding"></div>
- <video id="localVideo" ng-class="[videoMode, currentCall.state]" ng-show="currentCall && currentCall.type == 'video' && (currentCall.state == 'connected' || currentCall.state == 'connecting' || currentCall.state == 'invite_sent' || currentCall.state == 'ended')"></video>
- <video id="remoteVideo" ng-class="[videoMode, currentCall.state]" ng-show="currentCall && currentCall.type == 'video' && (currentCall.state == 'connected' || (currentCall.state == 'ended' && currentCall.didConnect))"></video>
+ <div ng-class="[videoMode, currentCall.state]" ng-show="currentCall && currentCall.type == 'video' && (currentCall.state == 'connected' || currentCall.state == 'connecting' || currentCall.state == 'invite_sent' || currentCall.state == 'ended')"><video id="localVideo"></video></div>
+ <div ng-class="[videoMode, currentCall.state]" ng-show="currentCall && currentCall.type == 'video' && (currentCall.state == 'connected' || (currentCall.state == 'ended' && currentCall.didConnect))"><video id="remoteVideo"></video></div>
</div>
</div>
@@ -60,8 +66,7 @@
<div id="headerContent" ng-hide="'/login' == location || '/register' == location">
<div id="callBar" ng-show="currentCall">
<img id="callPeerImage" ng-show="currentCall.userProfile.avatar_url" ngSrc="{{ currentCall.userProfile.avatar_url }}" />
- <img class="callIcon" src="img/green_phone.png" ng-show="currentCall.state != 'ended'" />
- <img class="callIcon" id="callEndedIcon" src="img/red_phone.png" ng-show="currentCall.state == 'ended'" />
+ <img class="callIcon" src="img/green_phone.png" ng-show="!!currentCall" ng-class="currentCall.state" />
<div id="callPeerNameAndState">
<span id="callPeerName">{{ currentCall.userProfile.displayname }}</span>
<br />
@@ -82,7 +87,7 @@
</span>
</div>
<span ng-show="currentCall.state == 'ringing'">
- <button ng-click="answerCall()" ng-disabled="!isWebRTCSupported" title="{{isWebRTCSupported ? '' : 'Your browser does not support VoIP' }}">Answer {{ currentCall.type }} call</button>
+ <button ng-click="answerCall()" ng-disabled="!isWebRTCSupported()" title="{{isWebRTCSupported() ? '' : 'Your browser does not support VoIP' }}">Answer {{ currentCall.type }} call</button>
<button ng-click="hangupCall()">Reject</button>
</span>
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
diff --git a/webclient/js/angular-animate.js b/syweb/webclient/js/angular-animate.js
index c15f793c1b..c15f793c1b 100644
--- a/webclient/js/angular-animate.js
+++ b/syweb/webclient/js/angular-animate.js
diff --git a/webclient/js/angular-animate.min.js b/syweb/webclient/js/angular-animate.min.js
index 1ce2a93ac7..1ce2a93ac7 100644
--- a/webclient/js/angular-animate.min.js
+++ b/syweb/webclient/js/angular-animate.min.js
diff --git a/webclient/js/angular-mocks.js b/syweb/webclient/js/angular-mocks.js
index 48c0b5decb..24bbcd4137 100755
--- a/webclient/js/angular-mocks.js
+++ b/syweb/webclient/js/angular-mocks.js
@@ -1,10 +1,3 @@
-/**
- * @license AngularJS v1.2.22
- * (c) 2010-2014 Google, Inc. http://angularjs.org
- * License: MIT
- */
-(function(window, angular, undefined) {
-
'use strict';
/**
@@ -63,6 +56,8 @@ angular.mock.$Browser = function() {
return listener;
};
+ self.$$checkUrlChange = angular.noop;
+
self.cookieHash = {};
self.lastCookieHash = {};
self.deferredFns = [];
@@ -125,7 +120,7 @@ angular.mock.$Browser = function() {
}
};
- self.$$baseHref = '';
+ self.$$baseHref = '/';
self.baseHref = function() {
return this.$$baseHref;
};
@@ -774,13 +769,22 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
};
});
- $provide.decorator('$animate', function($delegate, $$asyncCallback) {
+ $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser',
+ function($delegate, $$asyncCallback, $timeout, $browser) {
var animate = {
queue : [],
+ cancel : $delegate.cancel,
enabled : $delegate.enabled,
- triggerCallbacks : function() {
+ triggerCallbackEvents : function() {
$$asyncCallback.flush();
},
+ triggerCallbackPromise : function() {
+ $timeout.flush(0);
+ },
+ triggerCallbacks : function() {
+ this.triggerCallbackEvents();
+ this.triggerCallbackPromise();
+ },
triggerReflow : function() {
angular.forEach(reflowQueue, function(fn) {
fn();
@@ -797,12 +801,12 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
element : arguments[0],
args : arguments
});
- $delegate[method].apply($delegate, arguments);
+ return $delegate[method].apply($delegate, arguments);
};
});
return animate;
- });
+ }]);
}]);
@@ -888,7 +892,7 @@ angular.mock.dump = function(object) {
* development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}.
*
* During unit testing, we want our unit tests to run quickly and have no external dependencies so
- * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or
+ * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or
* [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is
* to verify whether a certain request has been sent or not, or alternatively just let the
* application make requests, respond with pre-trained responses and assert that the end result is
@@ -1007,13 +1011,14 @@ angular.mock.dump = function(object) {
```js
// testing controller
describe('MyController', function() {
- var $httpBackend, $rootScope, createController;
+ var $httpBackend, $rootScope, createController, authRequestHandler;
beforeEach(inject(function($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// backend definition common for all tests
- $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
+ authRequestHandler = $httpBackend.when('GET', '/auth.py')
+ .respond({userId: 'userX'}, {'A-Token': 'xxx'});
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
@@ -1039,11 +1044,23 @@ angular.mock.dump = function(object) {
});
+ it('should fail authentication', function() {
+
+ // Notice how you can change the response even after it was set
+ authRequestHandler.respond(401, '');
+
+ $httpBackend.expectGET('/auth.py');
+ var controller = createController();
+ $httpBackend.flush();
+ expect($rootScope.status).toBe('Failed...');
+ });
+
+
it('should send msg to server', function() {
var controller = createController();
$httpBackend.flush();
- // now you don’t care about the authentication, but
+ // now you don’t care about the authentication, but
// the controller will still send the request and
// $httpBackend will respond without you having to
// specify the expectation and response for this request
@@ -1186,32 +1203,39 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* Creates a new backend definition.
*
* @param {string} method HTTP method.
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*
- * - respond –
+ * - respond –
* `{function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers)}`
- * – The respond method takes a set of static data to be returned or a function that can
+ * – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (string), response
- * headers (Object), and the text for the status (string).
+ * headers (Object), and the text for the status (string). The respond method returns the
+ * `requestHandler` object for possible overrides.
*/
$httpBackend.when = function(method, url, data, headers) {
var definition = new MockHttpExpectation(method, url, data, headers),
chain = {
respond: function(status, data, headers, statusText) {
+ definition.passThrough = undefined;
definition.response = createResponse(status, data, headers, statusText);
+ return chain;
}
};
if ($browser) {
chain.passThrough = function() {
+ definition.response = undefined;
definition.passThrough = true;
+ return chain;
};
}
@@ -1225,10 +1249,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1237,10 +1263,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1249,10 +1277,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1261,12 +1291,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1275,12 +1307,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1289,9 +1323,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
createShortMethods('when');
@@ -1303,30 +1339,36 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* Creates a new request expectation.
*
* @param {string} method HTTP method.
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*
- * - respond –
+ * - respond –
* `{function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers)}`
- * – The respond method takes a set of static data to be returned or a function that can
+ * – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (string), response
- * headers (Object), and the text for the status (string).
+ * headers (Object), and the text for the status (string). The respond method returns the
+ * `requestHandler` object for possible overrides.
*/
$httpBackend.expect = function(method, url, data, headers) {
- var expectation = new MockHttpExpectation(method, url, data, headers);
+ var expectation = new MockHttpExpectation(method, url, data, headers),
+ chain = {
+ respond: function (status, data, headers, statusText) {
+ expectation.response = createResponse(status, data, headers, statusText);
+ return chain;
+ }
+ };
+
expectations.push(expectation);
- return {
- respond: function (status, data, headers, statusText) {
- expectation.response = createResponse(status, data, headers, statusText);
- }
- };
+ return chain;
};
@@ -1336,10 +1378,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new request expectation for GET requests. For more info see `expect()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled. See #expect for more info.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled. See #expect for more info.
*/
/**
@@ -1348,10 +1392,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new request expectation for HEAD requests. For more info see `expect()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1360,10 +1406,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new request expectation for DELETE requests. For more info see `expect()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1372,13 +1420,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new request expectation for POST requests. For more info see `expect()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1387,13 +1437,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new request expectation for PUT requests. For more info see `expect()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1402,13 +1454,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new request expectation for PATCH requests. For more info see `expect()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
/**
@@ -1417,9 +1471,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* @description
* Creates a new request expectation for JSONP requests. For more info see `expect()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
- * request is handled.
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled.
*/
createShortMethods('expect');
@@ -1434,11 +1490,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* all pending requests will be flushed. If there are no pending requests when the flush method
* is called an exception is thrown (as this typically a sign of programming error).
*/
- $httpBackend.flush = function(count) {
- $rootScope.$digest();
+ $httpBackend.flush = function(count, digest) {
+ if (digest !== false) $rootScope.$digest();
if (!responses.length) throw new Error('No pending request to flush !');
- if (angular.isDefined(count)) {
+ if (angular.isDefined(count) && count !== null) {
while (count--) {
if (!responses.length) throw new Error('No more pending request to flush !');
responses.shift()();
@@ -1448,7 +1504,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
responses.shift()();
}
}
- $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingExpectation(digest);
};
@@ -1466,8 +1522,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
* afterEach($httpBackend.verifyNoOutstandingExpectation);
* ```
*/
- $httpBackend.verifyNoOutstandingExpectation = function() {
- $rootScope.$digest();
+ $httpBackend.verifyNoOutstandingExpectation = function(digest) {
+ if (digest !== false) $rootScope.$digest();
if (expectations.length) {
throw new Error('Unsatisfied requests: ' + expectations.join(', '));
}
@@ -1511,7 +1567,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
function createShortMethods(prefix) {
- angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) {
+ angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) {
$httpBackend[prefix + method] = function(url, headers) {
return $httpBackend[prefix](method, url, undefined, headers);
};
@@ -1541,6 +1597,7 @@ function MockHttpExpectation(method, url, data, headers) {
this.matchUrl = function(u) {
if (!url) return true;
if (angular.isFunction(url.test)) return url.test(u);
+ if (angular.isFunction(url)) return url(u);
return url == u;
};
@@ -1627,7 +1684,7 @@ function MockXhr() {
* that adds a "flush" and "verifyNoPendingTasks" methods.
*/
-angular.mock.$TimeoutDecorator = function($delegate, $browser) {
+angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function ($delegate, $browser) {
/**
* @ngdoc method
@@ -1666,9 +1723,9 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) {
}
return $delegate;
-};
+}];
-angular.mock.$RAFDecorator = function($delegate) {
+angular.mock.$RAFDecorator = ['$delegate', function($delegate) {
var queue = [];
var rafFn = function(fn) {
var index = queue.length;
@@ -1694,9 +1751,9 @@ angular.mock.$RAFDecorator = function($delegate) {
};
return rafFn;
-};
+}];
-angular.mock.$AsyncCallbackDecorator = function($delegate) {
+angular.mock.$AsyncCallbackDecorator = ['$delegate', function($delegate) {
var callbacks = [];
var addFn = function(fn) {
callbacks.push(fn);
@@ -1708,7 +1765,7 @@ angular.mock.$AsyncCallbackDecorator = function($delegate) {
callbacks = [];
};
return addFn;
-};
+}];
/**
*
@@ -1822,22 +1879,25 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* Creates a new backend definition.
*
* @param {string} method HTTP method.
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*
- * - respond –
+ * - respond –
* `{function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers)}`
- * – The respond method takes a set of static data to be returned or a function that can return
+ * – The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string), response headers
* (Object), and the text for the status (string).
- * - passThrough – `{function()}` – Any request matching a backend definition with
+ * - passThrough – `{function()}` – Any request matching a backend definition with
* `passThrough` handler will be passed through to the real backend (an XHR request will be made
* to the server.)
+ * - Both methods return the `requestHandler` object for possible overrides.
*/
/**
@@ -1847,10 +1907,12 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
@@ -1860,10 +1922,12 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
@@ -1873,10 +1937,12 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
@@ -1886,11 +1952,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
@@ -1900,11 +1968,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
@@ -1914,11 +1984,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for PATCH requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
@@ -1928,30 +2000,17 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ * and returns true if the url match the current definition.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- * control how a matched request is handled.
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
*/
angular.mock.e2e = {};
angular.mock.e2e.$httpBackendDecorator =
['$rootScope', '$delegate', '$browser', createHttpBackendMock];
-angular.mock.clearDataCache = function() {
- var key,
- cache = angular.element.cache;
-
- for(key in cache) {
- if (Object.prototype.hasOwnProperty.call(cache,key)) {
- var handle = cache[key].handle;
-
- handle && angular.element(handle.elem).off();
- delete cache[key];
- }
- }
-};
-
-
if(window.jasmine || window.mocha) {
var currentSpec = null,
@@ -1982,8 +2041,6 @@ if(window.jasmine || window.mocha) {
injector.get('$browser').pollFns.length = 0;
}
- angular.mock.clearDataCache();
-
// clean up jquery's fragment cache
angular.forEach(angular.element.fragments, function(val, key) {
delete angular.element.fragments[key];
@@ -2003,6 +2060,7 @@ if(window.jasmine || window.mocha) {
* @description
*
* *NOTE*: This function is also published on window for easy access.<br>
+ * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha
*
* This function registers a module configuration code. It collects the configuration information
* which will be used when the injector is created by {@link angular.mock.inject inject}.
@@ -2045,6 +2103,7 @@ if(window.jasmine || window.mocha) {
* @description
*
* *NOTE*: This function is also published on window for easy access.<br>
+ * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha
*
* The inject function wraps a function into an injectable function. The inject() creates new
* instance of {@link auto.$injector $injector} per test, which is then used for
@@ -2144,14 +2203,28 @@ if(window.jasmine || window.mocha) {
/////////////////////
function workFn() {
var modules = currentSpec.$modules || [];
-
+ var strictDi = !!currentSpec.$injectorStrict;
modules.unshift('ngMock');
modules.unshift('ng');
var injector = currentSpec.$injector;
if (!injector) {
- injector = currentSpec.$injector = angular.injector(modules);
+ if (strictDi) {
+ // If strictDi is enabled, annotate the providerInjector blocks
+ angular.forEach(modules, function(moduleFn) {
+ if (typeof moduleFn === "function") {
+ angular.injector.$$annotate(moduleFn);
+ }
+ });
+ }
+ injector = currentSpec.$injector = angular.injector(modules, strictDi);
+ currentSpec.$injectorStrict = strictDi;
}
for(var i = 0, ii = blockFns.length; i < ii; i++) {
+ if (currentSpec.$injectorStrict) {
+ // If the injector is strict / strictDi, and the spec wants to inject using automatic
+ // annotation, then annotate the function here.
+ injector.annotate(blockFns[i]);
+ }
try {
/* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */
injector.invoke(blockFns[i] || angular.noop, this);
@@ -2167,7 +2240,20 @@ if(window.jasmine || window.mocha) {
}
}
};
-}
-})(window, window.angular);
\ No newline at end of file
+ angular.mock.inject.strictDi = function(value) {
+ value = arguments.length ? !!value : true;
+ return isSpecRunning() ? workFn() : workFn;
+
+ function workFn() {
+ if (value !== currentSpec.$injectorStrict) {
+ if (currentSpec.$injector) {
+ throw new Error('Injector already created, can not modify strict annotations');
+ } else {
+ currentSpec.$injectorStrict = value;
+ }
+ }
+ }
+ };
+}
diff --git a/syweb/webclient/js/angular-peity.js b/syweb/webclient/js/angular-peity.js
new file mode 100644
index 0000000000..2acb647d91
--- /dev/null
+++ b/syweb/webclient/js/angular-peity.js
@@ -0,0 +1,69 @@
+var angularPeity = angular.module( 'angular-peity', [] );
+
+$.fn.peity.defaults.pie = {
+ fill: ["#ff0000", "#aaaaaa"],
+ radius: 4,
+}
+
+var buildChartDirective = function ( chartType ) {
+ return {
+ restrict: 'E',
+ scope: {
+ data: "=",
+ options: "="
+ },
+ link: function ( scope, element, attrs ) {
+
+ var options = {};
+ if ( scope.options ) {
+ options = scope.options;
+ }
+
+ // N.B. live-binding to data by Matthew
+ scope.$watch('data', function () {
+ var span = document.createElement( 'span' );
+ span.textContent = scope.data.join();
+
+ if ( !attrs.class ) {
+ span.className = "";
+ } else {
+ span.className = attrs.class;
+ }
+
+ if (element[0].nodeType === 8) {
+ element.replaceWith( span );
+ }
+ else if (element[0].firstChild) {
+ element.empty();
+ element[0].appendChild( span );
+ }
+ else {
+ element[0].appendChild( span );
+ }
+
+ jQuery( span ).peity( chartType, options );
+ });
+ }
+ };
+};
+
+
+angularPeity.directive( 'pieChart', function () {
+
+ return buildChartDirective( "pie" );
+
+} );
+
+
+angularPeity.directive( 'barChart', function () {
+
+ return buildChartDirective( "bar" );
+
+} );
+
+
+angularPeity.directive( 'lineChart', function () {
+
+ return buildChartDirective( "line" );
+
+} );
diff --git a/webclient/js/angular-route.js b/syweb/webclient/js/angular-route.js
index 305d92e855..305d92e855 100644
--- a/webclient/js/angular-route.js
+++ b/syweb/webclient/js/angular-route.js
diff --git a/webclient/js/angular-route.min.js b/syweb/webclient/js/angular-route.min.js
index 03da279ec3..03da279ec3 100644
--- a/webclient/js/angular-route.min.js
+++ b/syweb/webclient/js/angular-route.min.js
diff --git a/webclient/js/angular-sanitize.js b/syweb/webclient/js/angular-sanitize.js
index ec46895f68..ec46895f68 100644
--- a/webclient/js/angular-sanitize.js
+++ b/syweb/webclient/js/angular-sanitize.js
diff --git a/webclient/js/angular-sanitize.min.js b/syweb/webclient/js/angular-sanitize.min.js
index ce99bba18e..ce99bba18e 100644
--- a/webclient/js/angular-sanitize.min.js
+++ b/syweb/webclient/js/angular-sanitize.min.js
diff --git a/webclient/js/angular.js b/syweb/webclient/js/angular.js
index bdc97abb02..bdc97abb02 100644
--- a/webclient/js/angular.js
+++ b/syweb/webclient/js/angular.js
diff --git a/webclient/js/angular.min.js b/syweb/webclient/js/angular.min.js
index 5475589e2f..5475589e2f 100644
--- a/webclient/js/angular.min.js
+++ b/syweb/webclient/js/angular.min.js
diff --git a/webclient/js/autofill-event.js b/syweb/webclient/js/autofill-event.js
index 006f83e1be..006f83e1be 100755
--- a/webclient/js/autofill-event.js
+++ b/syweb/webclient/js/autofill-event.js
diff --git a/syweb/webclient/js/elastic.js b/syweb/webclient/js/elastic.js
new file mode 100644
index 0000000000..d585d81109
--- /dev/null
+++ b/syweb/webclient/js/elastic.js
@@ -0,0 +1,216 @@
+/*
+ * angular-elastic v2.4.0
+ * (c) 2014 Monospaced http://monospaced.com
+ * License: MIT
+ */
+
+angular.module('monospaced.elastic', [])
+
+ .constant('msdElasticConfig', {
+ append: ''
+ })
+
+ .directive('msdElastic', [
+ '$timeout', '$window', 'msdElasticConfig',
+ function($timeout, $window, config) {
+ 'use strict';
+
+ return {
+ require: 'ngModel',
+ restrict: 'A, C',
+ link: function(scope, element, attrs, ngModel) {
+
+ // cache a reference to the DOM element
+ var ta = element[0],
+ $ta = element;
+
+ // ensure the element is a textarea, and browser is capable
+ if (ta.nodeName !== 'TEXTAREA' || !$window.getComputedStyle) {
+ return;
+ }
+
+ // set these properties before measuring dimensions
+ $ta.css({
+ 'overflow': 'hidden',
+ 'overflow-y': 'hidden',
+ 'word-wrap': 'break-word'
+ });
+
+ // force text reflow
+ var text = ta.value;
+ ta.value = '';
+ ta.value = text;
+
+ var append = attrs.msdElastic ? attrs.msdElastic.replace(/\\n/g, '\n') : config.append,
+ $win = angular.element($window),
+ mirrorInitStyle = 'position: absolute; top: -999px; right: auto; bottom: auto;' +
+ 'left: 0; overflow: hidden; -webkit-box-sizing: content-box;' +
+ '-moz-box-sizing: content-box; box-sizing: content-box;' +
+ 'min-height: 0 !important; height: 0 !important; padding: 0;' +
+ 'word-wrap: break-word; border: 0;',
+ $mirror = angular.element('<textarea tabindex="-1" ' +
+ 'style="' + mirrorInitStyle + '"/>').data('elastic', true),
+ mirror = $mirror[0],
+ taStyle = getComputedStyle(ta),
+ resize = taStyle.getPropertyValue('resize'),
+ borderBox = taStyle.getPropertyValue('box-sizing') === 'border-box' ||
+ taStyle.getPropertyValue('-moz-box-sizing') === 'border-box' ||
+ taStyle.getPropertyValue('-webkit-box-sizing') === 'border-box',
+ boxOuter = !borderBox ? {width: 0, height: 0} : {
+ width: parseInt(taStyle.getPropertyValue('border-right-width'), 10) +
+ parseInt(taStyle.getPropertyValue('padding-right'), 10) +
+ parseInt(taStyle.getPropertyValue('padding-left'), 10) +
+ parseInt(taStyle.getPropertyValue('border-left-width'), 10),
+ height: parseInt(taStyle.getPropertyValue('border-top-width'), 10) +
+ parseInt(taStyle.getPropertyValue('padding-top'), 10) +
+ parseInt(taStyle.getPropertyValue('padding-bottom'), 10) +
+ parseInt(taStyle.getPropertyValue('border-bottom-width'), 10)
+ },
+ minHeightValue = parseInt(taStyle.getPropertyValue('min-height'), 10),
+ heightValue = parseInt(taStyle.getPropertyValue('height'), 10),
+ minHeight = Math.max(minHeightValue, heightValue) - boxOuter.height,
+ maxHeight = parseInt(taStyle.getPropertyValue('max-height'), 10),
+ mirrored,
+ active,
+ copyStyle = ['font-family',
+ 'font-size',
+ 'font-weight',
+ 'font-style',
+ 'letter-spacing',
+ 'line-height',
+ 'text-transform',
+ 'word-spacing',
+ 'text-indent'];
+
+ // exit if elastic already applied (or is the mirror element)
+ if ($ta.data('elastic')) {
+ return;
+ }
+
+ // Opera returns max-height of -1 if not set
+ maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
+
+ // append mirror to the DOM
+ if (mirror.parentNode !== document.body) {
+ angular.element(document.body).append(mirror);
+ }
+
+ // set resize and apply elastic
+ $ta.css({
+ 'resize': (resize === 'none' || resize === 'vertical') ? 'none' : 'horizontal'
+ }).data('elastic', true);
+
+ /*
+ * methods
+ */
+
+ function initMirror() {
+ var mirrorStyle = mirrorInitStyle;
+
+ mirrored = ta;
+ // copy the essential styles from the textarea to the mirror
+ taStyle = getComputedStyle(ta);
+ angular.forEach(copyStyle, function(val) {
+ mirrorStyle += val + ':' + taStyle.getPropertyValue(val) + ';';
+ });
+ mirror.setAttribute('style', mirrorStyle);
+ }
+
+ function adjust() {
+ var taHeight,
+ taComputedStyleWidth,
+ mirrorHeight,
+ width,
+ overflow;
+
+ if (mirrored !== ta) {
+ initMirror();
+ }
+
+ // active flag prevents actions in function from calling adjust again
+ if (!active) {
+ active = true;
+
+ mirror.value = ta.value + append; // optional whitespace to improve animation
+ mirror.style.overflowY = ta.style.overflowY;
+
+ taHeight = ta.style.height === '' ? 'auto' : parseInt(ta.style.height, 10);
+
+ taComputedStyleWidth = getComputedStyle(ta).getPropertyValue('width');
+
+ // ensure getComputedStyle has returned a readable 'used value' pixel width
+ if (taComputedStyleWidth.substr(taComputedStyleWidth.length - 2, 2) === 'px') {
+ // update mirror width in case the textarea width has changed
+ width = parseInt(taComputedStyleWidth, 10) - boxOuter.width;
+ mirror.style.width = width + 'px';
+ }
+
+ mirrorHeight = mirror.scrollHeight;
+
+ if (mirrorHeight > maxHeight) {
+ mirrorHeight = maxHeight;
+ overflow = 'scroll';
+ } else if (mirrorHeight < minHeight) {
+ mirrorHeight = minHeight;
+ }
+ mirrorHeight += boxOuter.height;
+
+ ta.style.overflowY = overflow || 'hidden';
+
+ if (taHeight !== mirrorHeight) {
+ ta.style.height = mirrorHeight + 'px';
+ scope.$emit('elastic:resize', $ta);
+ }
+
+ // small delay to prevent an infinite loop
+ $timeout(function() {
+ active = false;
+ }, 1);
+
+ }
+ }
+
+ function forceAdjust() {
+ active = false;
+ adjust();
+ }
+
+ /*
+ * initialise
+ */
+
+ // listen
+ if ('onpropertychange' in ta && 'oninput' in ta) {
+ // IE9
+ ta['oninput'] = ta.onkeyup = adjust;
+ } else {
+ ta['oninput'] = adjust;
+ }
+
+ $win.bind('resize', forceAdjust);
+
+ scope.$watch(function() {
+ return ngModel.$modelValue;
+ }, function(newValue) {
+ forceAdjust();
+ });
+
+ scope.$on('elastic:adjust', function() {
+ initMirror();
+ forceAdjust();
+ });
+
+ $timeout(adjust);
+
+ /*
+ * destroy
+ */
+
+ scope.$on('$destroy', function() {
+ $mirror.remove();
+ $win.unbind('resize', forceAdjust);
+ });
+ }
+ };
+ }
+ ]);
diff --git a/webclient/js/jquery-1.8.3.min.js b/syweb/webclient/js/jquery-1.8.3.min.js
index 3883779527..3883779527 100644
--- a/webclient/js/jquery-1.8.3.min.js
+++ b/syweb/webclient/js/jquery-1.8.3.min.js
diff --git a/syweb/webclient/js/jquery.peity.min.js b/syweb/webclient/js/jquery.peity.min.js
new file mode 100644
index 0000000000..054b83c5d8
--- /dev/null
+++ b/syweb/webclient/js/jquery.peity.min.js
@@ -0,0 +1,13 @@
+// Peity jQuery plugin version 3.0.2
+// (c) 2014 Ben Pickles
+//
+// http://benpickles.github.io/peity
+//
+// Released under MIT license.
+(function(h,w,i,v){var p=function(a,b){var d=w.createElementNS("http://www.w3.org/2000/svg",a);h(d).attr(b);return d},y="createElementNS"in w&&p("svg",{}).createSVGRect,e=h.fn.peity=function(a,b){y&&this.each(function(){var d=h(this),c=d.data("peity");c?(a&&(c.type=a),h.extend(c.opts,b)):(c=new x(d,a,h.extend({},e.defaults[a],b)),d.change(function(){c.draw()}).data("peity",c));c.draw()});return this},x=function(a,b,d){this.$el=a;this.type=b;this.opts=d},r=x.prototype;r.draw=function(){e.graphers[this.type].call(this,
+this.opts)};r.fill=function(){var a=this.opts.fill;return h.isFunction(a)?a:function(b,d){return a[d%a.length]}};r.prepare=function(a,b){this.svg||this.$el.hide().after(this.svg=p("svg",{"class":"peity"}));return h(this.svg).empty().data("peity",this).attr({height:b,width:a})};r.values=function(){return h.map(this.$el.text().split(this.opts.delimiter),function(a){return parseFloat(a)})};e.defaults={};e.graphers={};e.register=function(a,b,d){this.defaults[a]=b;this.graphers[a]=d};e.register("pie",
+{fill:["#ff9900","#fff4dd","#ffc66e"],radius:8},function(a){if(!a.delimiter){var b=this.$el.text().match(/[^0-9\.]/);a.delimiter=b?b[0]:","}b=this.values();if("/"==a.delimiter)var d=b[0],b=[d,i.max(0,b[1]-d)];for(var c=0,d=b.length,n=0;c<d;c++)n+=b[c];for(var c=2*a.radius,f=this.prepare(a.width||c,a.height||c),c=f.width(),f=f.height(),s=c/2,k=f/2,f=i.min(s,k),a=a.innerRadius,e=i.PI,q=this.fill(),g=this.scale=function(a,b){var c=2*a/n*e-e/2;return[b*i.cos(c)+s,b*i.sin(c)+k]},l=0,c=0;c<d;c++){var t=
+b[c],j=t/n;if(0!=j){if(1==j)if(a)var j=s-0.01,o=k-f,m=k-a,j=p("path",{d:["M",s,o,"A",f,f,0,1,1,j,o,"L",j,m,"A",a,a,0,1,0,s,m].join(" ")});else j=p("circle",{cx:s,cy:k,r:f});else o=l+t,m=["M"].concat(g(l,f),"A",f,f,0,0.5<j?1:0,1,g(o,f),"L"),a?m=m.concat(g(o,a),"A",a,a,0,0.5<j?1:0,0,g(l,a)):m.push(s,k),l+=t,j=p("path",{d:m.join(" ")});h(j).attr("fill",q.call(this,t,c,b));this.svg.appendChild(j)}}});e.register("donut",h.extend(!0,{},e.defaults.pie),function(a){a.innerRadius||(a.innerRadius=0.5*a.radius);
+e.graphers.pie.call(this,a)});e.register("line",{delimiter:",",fill:"#c6d9fd",height:16,min:0,stroke:"#4d89f9",strokeWidth:1,width:32},function(a){var b=this.values();1==b.length&&b.push(b[0]);for(var d=i.max.apply(i,a.max==v?b:b.concat(a.max)),c=i.min.apply(i,a.min==v?b:b.concat(a.min)),n=this.prepare(a.width,a.height),f=a.strokeWidth,e=n.width(),k=n.height()-f,h=d-c,d=this.x=function(a){return a*(e/(b.length-1))},n=this.y=function(a){var b=k;h&&(b-=(a-c)/h*k);return b+f/2},q=n(i.max(c,0)),g=[0,
+q],l=0;l<b.length;l++)g.push(d(l),n(b[l]));g.push(e,q);this.svg.appendChild(p("polygon",{fill:a.fill,points:g.join(" ")}));f&&this.svg.appendChild(p("polyline",{fill:"transparent",points:g.slice(2,g.length-2).join(" "),stroke:a.stroke,"stroke-width":f,"stroke-linecap":"square"}))});e.register("bar",{delimiter:",",fill:["#4D89F9"],height:16,min:0,padding:0.1,width:32},function(a){for(var b=this.values(),d=i.max.apply(i,a.max==v?b:b.concat(a.max)),c=i.min.apply(i,a.min==v?b:b.concat(a.min)),e=this.prepare(a.width,
+a.height),f=e.width(),h=e.height(),k=d-c,a=a.padding,e=this.fill(),r=this.x=function(a){return a*f/b.length},q=this.y=function(a){return h-(k?(a-c)/k*h:1)},g=0;g<b.length;g++){var l=r(g+a),t=r(g+1-a)-l,j=b[g],o=q(j),m=o,u;k?0>j?m=q(i.min(d,0)):o=q(i.max(c,0)):u=1;u=o-m;0==u&&(u=1,0<d&&k&&m--);this.svg.appendChild(p("rect",{fill:e.call(this,j,g,b),x:l,y:m,width:t,height:u}))}})})(jQuery,document,Math);
diff --git a/webclient/js/ng-infinite-scroll-matrix.js b/syweb/webclient/js/ng-infinite-scroll-matrix.js
index 045ec8d93e..045ec8d93e 100644
--- a/webclient/js/ng-infinite-scroll-matrix.js
+++ b/syweb/webclient/js/ng-infinite-scroll-matrix.js
diff --git a/webclient/js/ui-bootstrap-tpls-0.11.2.js b/syweb/webclient/js/ui-bootstrap-tpls-0.11.2.js
index 260c2769b8..260c2769b8 100644
--- a/webclient/js/ui-bootstrap-tpls-0.11.2.js
+++ b/syweb/webclient/js/ui-bootstrap-tpls-0.11.2.js
diff --git a/webclient/login/login-controller.js b/syweb/webclient/login/login-controller.js
index 5ef39a7122..5ef39a7122 100644
--- a/webclient/login/login-controller.js
+++ b/syweb/webclient/login/login-controller.js
diff --git a/webclient/login/login.html b/syweb/webclient/login/login.html
index 6b321f8fc5..6b321f8fc5 100644
--- a/webclient/login/login.html
+++ b/syweb/webclient/login/login.html
diff --git a/webclient/login/register-controller.js b/syweb/webclient/login/register-controller.js
index be970ce1c3..b23a72b185 100644
--- a/webclient/login/register-controller.js
+++ b/syweb/webclient/login/register-controller.js
@@ -124,7 +124,7 @@ angular.module('RegisterController', ['matrixService'])
$location.url("home");
},
function(error) {
- console.trace("Registration error: "+error);
+ console.error("Registration error: "+JSON.stringify(error));
if (useCaptcha) {
Recaptcha.reload();
}
diff --git a/webclient/login/register.html b/syweb/webclient/login/register.html
index a27f9ad4e8..a27f9ad4e8 100644
--- a/webclient/login/register.html
+++ b/syweb/webclient/login/register.html
diff --git a/webclient/media/busy.mp3 b/syweb/webclient/media/busy.mp3
index fec27ba4c5..fec27ba4c5 100644
--- a/webclient/media/busy.mp3
+++ b/syweb/webclient/media/busy.mp3
Binary files differdiff --git a/webclient/media/busy.ogg b/syweb/webclient/media/busy.ogg
index 5d64a7d0d9..5d64a7d0d9 100644
--- a/webclient/media/busy.ogg
+++ b/syweb/webclient/media/busy.ogg
Binary files differdiff --git a/webclient/media/callend.mp3 b/syweb/webclient/media/callend.mp3
index 50c34e5640..50c34e5640 100644
--- a/webclient/media/callend.mp3
+++ b/syweb/webclient/media/callend.mp3
Binary files differdiff --git a/webclient/media/callend.ogg b/syweb/webclient/media/callend.ogg
index 927ce1f634..927ce1f634 100644
--- a/webclient/media/callend.ogg
+++ b/syweb/webclient/media/callend.ogg
Binary files differdiff --git a/webclient/media/ring.mp3 b/syweb/webclient/media/ring.mp3
index 3c3cdde3f9..3c3cdde3f9 100644
--- a/webclient/media/ring.mp3
+++ b/syweb/webclient/media/ring.mp3
Binary files differdiff --git a/webclient/media/ring.ogg b/syweb/webclient/media/ring.ogg
index de49b8ae6f..de49b8ae6f 100644
--- a/webclient/media/ring.ogg
+++ b/syweb/webclient/media/ring.ogg
Binary files differdiff --git a/webclient/media/ringback.mp3 b/syweb/webclient/media/ringback.mp3
index 6ee34bf395..6ee34bf395 100644
--- a/webclient/media/ringback.mp3
+++ b/syweb/webclient/media/ringback.mp3
Binary files differdiff --git a/webclient/media/ringback.ogg b/syweb/webclient/media/ringback.ogg
index 7dbfdcd017..7dbfdcd017 100644
--- a/webclient/media/ringback.ogg
+++ b/syweb/webclient/media/ringback.ogg
Binary files differdiff --git a/webclient/mobile.css b/syweb/webclient/mobile.css
index 6fa9221ccf..32b01c503d 100644
--- a/webclient/mobile.css
+++ b/syweb/webclient/mobile.css
@@ -1,4 +1,13 @@
/*** Mobile voodoo ***/
+
+/** iPads **/
+@media all and (max-device-width: 768px) {
+ #roomRecentsTableWrapper {
+ display: none;
+ }
+}
+
+/** iPhones **/
@media all and (max-device-width: 640px) {
#messageTableWrapper {
@@ -37,11 +46,16 @@
max-width: 640px ! important;
}
+ #controls {
+ padding: 0px;
+ }
+
#headerUserId,
#roomHeader img,
#userIdCell,
#roomRecentsTableWrapper,
#usersTableWrapper,
+ #controlButtons,
.extraControls {
display: none;
}
@@ -64,6 +78,10 @@
padding-top: 10px;
}
+ .roomHeaderInfo {
+ margin-right: 0px;
+ }
+
#roomName {
font-size: 12px ! important;
margin-top: 0px ! important;
diff --git a/syweb/webclient/recents/recents-controller.js b/syweb/webclient/recents/recents-controller.js
new file mode 100644
index 0000000000..41720d4cb0
--- /dev/null
+++ b/syweb/webclient/recents/recents-controller.js
@@ -0,0 +1,53 @@
+/*
+ Copyright 2014 OpenMarket Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+'use strict';
+
+angular.module('RecentsController', ['matrixService', 'matrixFilter'])
+.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService', 'recentsService',
+ function($rootScope, $scope, eventHandlerService, modelService, recentsService) {
+
+ // Expose the service to the view
+ $scope.eventHandlerService = eventHandlerService;
+
+ // retrieve all rooms and expose them
+ $scope.rooms = modelService.getRooms();
+
+ // track the selected room ID: the html will use this
+ $scope.recentsSelectedRoomID = recentsService.getSelectedRoomId();
+ $scope.$on(recentsService.BROADCAST_SELECTED_ROOM_ID, function(ngEvent, room_id) {
+ $scope.recentsSelectedRoomID = room_id;
+ });
+
+ // track the list of unread messages: the html will use this
+ $scope.unreadMessages = recentsService.getUnreadMessages();
+ $scope.$on(recentsService.BROADCAST_UNREAD_MESSAGES, function(ngEvent, room_id, unreadCount) {
+ $scope.unreadMessages = recentsService.getUnreadMessages();
+ });
+
+ // track the list of unread BING messages: the html will use this
+ $scope.unreadBings = recentsService.getUnreadBingMessages();
+ $scope.$on(recentsService.BROADCAST_UNREAD_BING_MESSAGES, function(ngEvent, room_id, event) {
+ $scope.unreadBings = recentsService.getUnreadBingMessages();
+ });
+
+ $scope.selectRoom = function(room) {
+ recentsService.markAsRead(room.room_id);
+ $rootScope.goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) );
+ };
+
+}]);
+
diff --git a/webclient/recents/recents-filter.js b/syweb/webclient/recents/recents-filter.js
index ef8d9897f7..cfbc6f4bd8 100644
--- a/webclient/recents/recents-filter.js
+++ b/syweb/webclient/recents/recents-filter.js
@@ -17,7 +17,7 @@
'use strict';
angular.module('RecentsController')
-.filter('orderRecents', ["matrixService", "eventHandlerService", function(matrixService, eventHandlerService) {
+.filter('orderRecents', ["matrixService", "eventHandlerService", "modelService", function(matrixService, eventHandlerService, modelService) {
return function(rooms) {
var user_id = matrixService.config().user_id;
@@ -25,26 +25,33 @@ angular.module('RecentsController')
// The key, room_id, is already in value objects
var filtered = [];
angular.forEach(rooms, function(room, room_id) {
-
+ room.recent = {};
+ var meEvent = room.current_room_state.state("m.room.member", user_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)) {
-
+ var member = modelService.getMember(room_id, user_id);
+ if (member) {
+ member = member.event;
+ }
+ room.recent.me = member;
+ if (member && ("invite" === member.content.membership || "join" === member.content.membership)) {
+ if ("invite" === member.content.membership) {
+ room.recent.inviter = member.user_id;
+ }
// Count users here
// TODO: Compute it directly in eventHandlerService
- room.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id);
+ room.recent.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id);
filtered.push(room);
}
- else if ("invite" === room.membership) {
+ else if (meEvent && "invite" === meEvent.content.membership) {
// The only information we have about the room is that the user has been invited
filtered.push(room);
}
});
// And time sort them
- // The room with the lastest message at first
+ // The room with the latest message at first
filtered.sort(function (roomA, roomB) {
var lastMsgRoomA = eventHandlerService.getLastMessage(roomA.room_id, true);
diff --git a/webclient/recents/recents.html b/syweb/webclient/recents/recents.html
index a52b215c7e..0b3a77ca11 100644
--- a/webclient/recents/recents.html
+++ b/syweb/webclient/recents/recents.html
@@ -1,16 +1,16 @@
<div ng-controller="RecentsController">
<table class="recentsTable">
- <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)}">
+ <tbody ng-repeat="(index, room) in rooms | orderRecents"
+ ng-click="selectRoom(room)"
+ class="recentsRoom"
+ ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID), 'recentsRoomBing': (unreadBings[room.room_id]), 'recentsRoomUnread': (unreadMessages[room.room_id])}">
<tr>
- <td ng-class="room['m.room.join_rules'].content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
+ <td ng-class="room.current_room_state.state('m.room.join_rules').content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
{{ room.room_id | mRoomName }}
</td>
<td class="recentsRoomSummaryUsersCount">
- <span ng-show="undefined !== room.numUsersInRoom">
- {{ room.numUsersInRoom || '1' }} {{ room.numUsersInRoom == 1 ? 'user' : 'users' }}
+ <span ng-show="undefined !== room.recent.numUsersInRoom">
+ {{ room.recent.numUsersInRoom || '1' }} {{ room.recent.numUsersInRoom == 1 ? 'user' : 'users' }}
</span>
</td>
<td class="recentsRoomSummaryTS">
@@ -27,11 +27,11 @@
<tr>
<td colspan="3" class="recentsRoomSummary">
- <div ng-show="room.membership === 'invite'">
- {{ room.inviter | mUserDisplayName: room.room_id }} invited you
+ <div ng-show="room.recent.me.content.membership === 'invite'">
+ {{ room.recent.inviter | mUserDisplayName: room.room_id }} invited you
</div>
- <div ng-hide="room.membership === 'invite'" ng-switch="lastMsg.type">
+ <div ng-hide="room.recent.me.membership === 'invite'" ng-switch="lastMsg.type">
<div ng-switch-when="m.room.member">
<span ng-switch="lastMsg.changedKey">
<span ng-switch-when="membership">
diff --git a/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js
index 841b5cccdd..67372a804f 100644
--- a/webclient/room/room-controller.js
+++ b/syweb/webclient/room/room-controller.js
@@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
-.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
- function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
+angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'angular-peity'])
+.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'modelService', 'recentsService', 'commandsService',
+ function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, modelService, recentsService, commandsService) {
'use strict';
var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320;
+
+ // .html needs this
+ $scope.containsBingWord = eventHandlerService.eventContainsBingWord;
// Room ids. Computed and resolved in onInit
$scope.room_id = undefined;
@@ -36,12 +39,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
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;
- $scope.autoCompleteIndex = 0;
- $scope.autoCompleteOriginal = "";
$scope.imageURLToSend = "";
- $scope.userIDToInvite = "";
// vars and functions for updating the name
@@ -54,7 +53,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
return;
};
- var nameEvent = $rootScope.events.rooms[$scope.room_id]['m.room.name'];
+ var nameEvent = $scope.room.current_room_state.state_events['m.room.name'];
if (nameEvent) {
$scope.name.newNameText = nameEvent.content.name;
}
@@ -95,7 +94,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
console.log("Warning: Already editing topic.");
return;
}
- var topicEvent = $rootScope.events.rooms[$scope.room_id]['m.room.topic'];
+ var topicEvent = $scope.room.current_room_state.state_events['m.room.topic'];
if (topicEvent) {
$scope.topic.newTopicText = topicEvent.content.topic;
}
@@ -152,7 +151,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
if (isLive && event.room_id === $scope.room_id) {
-
scrollToBottom();
}
});
@@ -187,21 +185,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
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) {
- 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);
- }
}
}
});
@@ -240,11 +223,11 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$scope.state.paginating = true;
}
- console.log("paginateBackMessages from " + $rootScope.events.rooms[$scope.room_id].pagination.earliest_token + " for " + numItems);
+ console.log("paginateBackMessages from " + $scope.room.old_room_state.pagination_token + " for " + numItems);
var originalTopRow = $("#messageTable>tbody>tr:first")[0];
// Paginate events from the point in cache
- matrixService.paginateBackMessages($scope.room_id, $rootScope.events.rooms[$scope.room_id].pagination.earliest_token, numItems).then(
+ matrixService.paginateBackMessages($scope.room_id, $scope.room.old_room_state.pagination_token, numItems).then(
function(response) {
eventHandlerService.handleRoomMessages($scope.room_id, response.data, false, 'b');
@@ -327,8 +310,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
}
$scope.members[target_user_id] = chunk;
- if (target_user_id in $rootScope.presence) {
- updatePresence($rootScope.presence[target_user_id]);
+ var usr = modelService.getUser(target_user_id);
+ if (usr) {
+ updatePresence(usr.event);
}
}
else {
@@ -390,7 +374,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
var updateUserPowerLevel = function(user_id) {
var member = $scope.members[user_id];
if (member) {
- member.powerLevel = matrixService.getUserPowerLevel($scope.room_id, user_id);
+ member.powerLevel = eventHandlerService.getUserPowerLevel($scope.room_id, user_id);
normaliseMembersPowerLevels();
}
@@ -431,172 +415,25 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
scrollToBottom(true);
// Store the command in the history
- history.push(input);
+ $rootScope.$broadcast("commandHistory:BROADCAST_NEW_HISTORY_ITEM(item)",
+ input);
+ var isEmote = input.indexOf("/me ") === 0;
var promise;
- var cmd;
- var args;
+ if (!isEmote) {
+ promise = commandsService.processInput($scope.room_id, input);
+ }
var echo = false;
- // Check for IRC style commands first
- // trim any trailing whitespace, as it can confuse the parser for IRC-style commands
- input = input.replace(/\s+$/, "");
-
- if (input[0] === "/" && input[1] !== "/") {
- var bits = input.match(/^(\S+?)( +(.*))?$/);
- cmd = bits[1];
- args = bits[3];
-
- console.log("cmd: " + cmd + ", args: " + args);
-
- switch (cmd) {
- case "/me":
- promise = matrixService.sendEmoteMessage($scope.room_id, args);
- echo = true;
- break;
-
- case "/nick":
- // Change user display name
- if (args) {
- promise = matrixService.setDisplayName(args);
- }
- else {
- $scope.feedback = "Usage: /nick <display_name>";
- }
- break;
-
- case "/join":
- // Join a room
- if (args) {
- var matches = args.match(/^(\S+)$/);
- if (matches) {
- var room_alias = matches[1];
- if (room_alias.indexOf(':') == -1) {
- // FIXME: actually track the :domain style name of our homeserver
- // with or without port as is appropriate and append it at this point
- }
-
- var room_id = matrixService.getAliasToRoomIdMapping(room_alias);
- console.log("joining " + room_alias + " id=" + room_id);
- if ($rootScope.events.rooms[room_id]) {
- // don't send a join event for a room you're already in.
- $location.url("room/" + room_alias);
- }
- else {
- promise = matrixService.joinAlias(room_alias).then(
- function(response) {
- // TODO: factor out the common housekeeping whenever we try to join a room or alias
- matrixService.roomState(response.room_id).then(
- function(response) {
- eventHandlerService.handleEvents(response.data, false, true);
- },
- function(error) {
- $scope.feedback = "Failed to get room state for: " + response.room_id;
- }
- );
- $location.url("room/" + room_alias);
- },
- function(error) {
- $scope.feedback = "Can't join room: " + JSON.stringify(error.data);
- }
- );
- }
- }
- }
- else {
- $scope.feedback = "Usage: /join <room_alias>";
- }
- break;
-
- case "/kick":
- // Kick a user from the room with an optional reason
- if (args) {
- var matches = args.match(/^(\S+?)( +(.*))?$/);
- if (matches) {
- promise = matrixService.kick($scope.room_id, matches[1], matches[3]);
- }
- }
-
- if (!promise) {
- $scope.feedback = "Usage: /kick <userId> [<reason>]";
- }
- break;
-
- case "/ban":
- // Ban a user from the room with an optional reason
- if (args) {
- var matches = args.match(/^(\S+?)( +(.*))?$/);
- if (matches) {
- promise = matrixService.ban($scope.room_id, matches[1], matches[3]);
- }
- }
-
- if (!promise) {
- $scope.feedback = "Usage: /ban <userId> [<reason>]";
- }
- break;
-
- case "/unban":
- // Unban a user from the room
- if (args) {
- var matches = args.match(/^(\S+)$/);
- if (matches) {
- // Reset the user membership to "leave" to unban him
- promise = matrixService.unban($scope.room_id, matches[1]);
- }
- }
-
- if (!promise) {
- $scope.feedback = "Usage: /unban <userId>";
- }
- break;
-
- case "/op":
- // Define the power level of a user
- if (args) {
- var matches = args.match(/^(\S+?)( +(\d+))?$/);
- var powerLevel = 50; // default power level for op
- if (matches) {
- var user_id = matches[1];
- if (matches.length === 4 && undefined !== matches[3]) {
- powerLevel = parseInt(matches[3]);
- }
- if (powerLevel !== NaN) {
- promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel);
- }
- }
- }
-
- if (!promise) {
- $scope.feedback = "Usage: /op <userId> [<power level>]";
- }
- break;
-
- case "/deop":
- // Reset the power level of a user
- if (args) {
- var matches = args.match(/^(\S+)$/);
- if (matches) {
- promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined);
- }
- }
-
- if (!promise) {
- $scope.feedback = "Usage: /deop <userId>";
- }
- break;
-
- default:
- $scope.feedback = ("Unrecognised IRC-style command: " + cmd);
- break;
- }
- }
- // By default send this as a message unless it's an IRC-style command
- if (!promise && !cmd) {
- // Make the request
- promise = matrixService.sendTextMessage($scope.room_id, input);
+ if (!promise) { // not a non-echoable command
echo = true;
+ if (isEmote) {
+ promise = matrixService.sendEmoteMessage($scope.room_id, input.substring(4));
+ }
+ else {
+ promise = matrixService.sendTextMessage($scope.room_id, input);
+ }
}
if (echo) {
@@ -604,8 +441,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
// To do so, create a minimalist fake text message event and add it to the in-memory list of room messages
var echoMessage = {
content: {
- body: (cmd === "/me" ? args : input),
- msgtype: (cmd === "/me" ? "m.emote" : "m.text"),
+ body: (isEmote ? input.substring(4) : input),
+ msgtype: (isEmote ? "m.emote" : "m.text"),
},
origin_server_ts: new Date().getTime(), // fake a timestamp
room_id: $scope.room_id,
@@ -615,7 +452,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
};
$('#mainInput').val('');
- $rootScope.events.rooms[$scope.room_id].messages.push(echoMessage);
+ $scope.room.addMessageEvent(echoMessage);
scrollToBottom();
}
@@ -638,7 +475,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
}
},
function(error) {
- $scope.feedback = "Request failed: " + error.data.error;
+ $scope.feedback = error.data.error;
if (echoMessage) {
// Mark the message as unsent for the rest of the page life
@@ -661,7 +498,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
if (room_id_or_alias && '!' === room_id_or_alias[0]) {
// Yes. We can go on right now
$scope.room_id = room_id_or_alias;
- $scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
+ $scope.room_alias = modelService.getRoomIdToAliasMapping($scope.room_id);
onInit2();
}
else {
@@ -703,6 +540,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
var onInit2 = function() {
console.log("onInit2");
+ // =============================
+ $scope.room = modelService.getRoom($scope.room_id);
+ // =============================
// Scroll down as soon as possible so that we point to the last message
// if it already exists in memory
@@ -715,9 +555,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
var needsToJoin = true;
// The room members is available in the data fetched by initialSync
- if ($rootScope.events.rooms[$scope.room_id]) {
+ if ($scope.room) {
- var messages = $rootScope.events.rooms[$scope.room_id].messages;
+ var messages = $scope.room.events;
if (0 === messages.length
|| (1 === messages.length && "m.room.member" === messages[0].type && "invite" === messages[0].content.membership && $scope.state.user_id === messages[0].state_key)) {
@@ -729,19 +569,19 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$scope.state.first_pagination = false;
}
- var members = $rootScope.events.rooms[$scope.room_id].members;
+ var members = $scope.room.current_room_state.members;
// Update the member list
for (var i in members) {
if (!members.hasOwnProperty(i)) continue;
- var member = members[i];
+ var member = members[i].event;
updateMemberList(member);
}
// Check if the user has already join the room
if ($scope.state.user_id in members) {
- if ("join" === members[$scope.state.user_id].membership) {
+ if ("join" === members[$scope.state.user_id].event.content.membership) {
needsToJoin = false;
}
}
@@ -785,10 +625,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
console.log("onInit3");
// Make recents highlight the current room
- $scope.recentsSelectedRoomID = $scope.room_id;
-
- // Init the history for this room
- history.init();
+ recentsService.setSelectedRoomId($scope.room_id);
// Get the up-to-date the current member list
matrixService.getMemberList($scope.room_id).then(
@@ -822,19 +659,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
}
);
};
-
- $scope.inviteUser = function() {
-
- matrixService.invite($scope.room_id, $scope.userIDToInvite).then(
- function() {
- console.log("Invited.");
- $scope.feedback = "Invite successfully sent to " + $scope.userIDToInvite;
- $scope.userIDToInvite = "";
- },
- function(reason) {
- $scope.feedback = "Failure: " + reason.data.error;
- });
- };
$scope.leaveRoom = function() {
@@ -886,109 +710,51 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
paginate(MESSAGES_PER_PAGINATION);
};
- $scope.startVoiceCall = function() {
+ $scope.checkWebRTC = function() {
+ if (!$rootScope.isWebRTCSupported()) {
+ alert("Your browser does not support WebRTC");
+ return false;
+ }
+ if ($scope.memberCount() != 2) {
+ alert("WebRTC calls are currently only supported on rooms with two members");
+ return false;
+ }
+ return true;
+ };
+
+ $scope.startVoiceCall = function() {
+ if (!$scope.checkWebRTC()) return;
var call = new MatrixCall($scope.room_id);
call.onError = $rootScope.onCallError;
call.onHangup = $rootScope.onCallHangup;
// remote video element is used for playing audio in voice calls
- call.remoteVideoElement = angular.element('#remoteVideo')[0];
+ call.remoteVideoSelector = angular.element('#remoteVideo')[0];
call.placeVoiceCall();
$rootScope.currentCall = call;
};
$scope.startVideoCall = function() {
+ if (!$scope.checkWebRTC()) return;
+
var call = new MatrixCall($scope.room_id);
call.onError = $rootScope.onCallError;
call.onHangup = $rootScope.onCallHangup;
- call.localVideoElement = angular.element('#localVideo')[0];
- call.remoteVideoElement = angular.element('#remoteVideo')[0];
+ call.localVideoSelector = '#localVideo';
+ call.remoteVideoSelector = '#remoteVideo';
call.placeVideoCall();
$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 = $('#mainInput').val();
- }
- else {
- // If the user modified this line in history, keep the change
- this.data[this.position] = $('#mainInput').val();
- }
-
- // 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
- $('#mainInput').val(this.data[this.position]);
- }
- else if (undefined !== this.typingMessage) {
- // Go back to the message the user started to type
- $('#mainInput').val(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();
- }
- };
-
$scope.openJson = function(content) {
- $scope.event_selected = content;
+ $scope.event_selected = angular.copy(content);
+
+ // FIXME: Pre-calculated event data should be stripped in a nicer way.
+ $scope.event_selected.__room_member = undefined;
+ $scope.event_selected.__target_room_member = undefined;
+
// scope this so the template can check power levels and enable/disable
// buttons
- $scope.pow = matrixService.getUserPowerLevel;
+ $scope.pow = eventHandlerService.getUserPowerLevel;
var modalInstance = $modal.open({
templateUrl: 'eventInfoTemplate.html',
@@ -1017,13 +783,70 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
});
};
+ $scope.openRoomInfo = function() {
+ $scope.roomInfo = {};
+ $scope.roomInfo.newEvent = {
+ content: {},
+ type: "",
+ state_key: ""
+ };
+
+ var stateEvents = $scope.room.current_room_state.state_events;
+ // The modal dialog will 2-way bind this field, so we MUST make a deep
+ // copy of the state events else we will be *actually adjusing our view
+ // of the world* when fiddling with the JSON!! Apparently parse/stringify
+ // is faster than jQuery's extend when doing deep copies.
+ $scope.roomInfo.stateEvents = JSON.parse(JSON.stringify(stateEvents));
+ var modalInstance = $modal.open({
+ templateUrl: 'roomInfoTemplate.html',
+ controller: 'RoomInfoController',
+ size: 'lg',
+ scope: $scope
+ });
+ };
+
}])
.controller('EventInfoController', function($scope, $modalInstance) {
console.log("Displaying modal dialog for >>>> " + JSON.stringify($scope.event_selected));
$scope.redact = function() {
console.log("User level = "+$scope.pow($scope.room_id, $scope.state.user_id)+
- " Redact level = "+$scope.events.rooms[$scope.room_id]["m.room.ops_levels"].content.redact_level);
+ " Redact level = "+$scope.room.current_room_state.state_events["m.room.ops_levels"].content.redact_level);
console.log("Redact event >> " + JSON.stringify($scope.event_selected));
$modalInstance.close("redact");
};
+ $scope.dismiss = $modalInstance.dismiss;
+})
+.controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) {
+ console.log("Displaying room info.");
+
+ $scope.userIDToInvite = "";
+
+ $scope.inviteUser = function() {
+
+ matrixService.invite($scope.room_id, $scope.userIDToInvite).then(
+ function() {
+ console.log("Invited.");
+ $scope.feedback = "Invite successfully sent to " + $scope.userIDToInvite;
+ $scope.userIDToInvite = "";
+ },
+ function(reason) {
+ $scope.feedback = "Failure: " + reason.data.error;
+ });
+ };
+
+ $scope.submit = function(event) {
+ if (event.content) {
+ console.log("submit >>> " + JSON.stringify(event.content));
+ matrixService.sendStateEvent($scope.room_id, event.type,
+ event.content, event.state_key).then(function(response) {
+ $modalInstance.dismiss();
+ }, function(err) {
+ $scope.feedback = err.data.error;
+ }
+ );
+ }
+ };
+
+ $scope.dismiss = $modalInstance.dismiss;
+
});
diff --git a/webclient/room/room-directive.js b/syweb/webclient/room/room-directive.js
index 05382cfcd3..187032aa88 100644
--- a/webclient/room/room-directive.js
+++ b/syweb/webclient/room/room-directive.js
@@ -144,19 +144,106 @@ angular.module('RoomController')
});
};
}])
+// A directive which stores text sent into it and restores it via up/down arrows
.directive('commandHistory', [ function() {
- return function (scope, element, attrs) {
- element.bind("keydown", function (event) {
- var keycodePressed = event.which;
- var UP_ARROW = 38;
- var DOWN_ARROW = 40;
- if (keycodePressed === UP_ARROW) {
- scope.history.goUp(event);
+ var BROADCAST_NEW_HISTORY_ITEM = "commandHistory:BROADCAST_NEW_HISTORY_ITEM(item)";
+
+ // Manage history of typed messages
+ // History is saved in sessionStorage 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,
+
+ element: undefined,
+ roomId: undefined,
+
+ // The message the user has started to type before going into the history
+ typingMessage: undefined,
+
+ // Init/load data for the current room
+ init: function(element, roomId) {
+ this.roomId = roomId;
+ this.element = element;
+ var data = sessionStorage.getItem("history_" + this.roomId);
+ if (data) {
+ this.data = JSON.parse(data);
}
- else if (keycodePressed === DOWN_ARROW) {
- scope.history.goDown(event);
- }
- });
+ },
+
+ // Store a message in the history
+ push: function(message) {
+ this.data.unshift(message);
+
+ // Update the session storage
+ sessionStorage.setItem("history_" + this.roomId, 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 = this.element.val();
+ }
+ else {
+ // If the user modified this line in history, keep the change
+ this.data[this.position] = this.element.val();
+ }
+
+ // 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
+ this.element.val(this.data[this.position]);
+ }
+ else if (undefined !== this.typingMessage) {
+ // Go back to the message the user started to type
+ this.element.val(this.typingMessage);
+ }
+ }
+ };
+
+ return {
+ restrict: "AE",
+ scope: {
+ roomId: "=commandHistory"
+ },
+ link: function (scope, element, attrs) {
+ element.bind("keydown", function (event) {
+ var keycodePressed = event.which;
+ var UP_ARROW = 38;
+ var DOWN_ARROW = 40;
+ if (scope.roomId) {
+ if (keycodePressed === UP_ARROW) {
+ history.go(1);
+ event.preventDefault();
+ }
+ else if (keycodePressed === DOWN_ARROW) {
+ history.go(-1);
+ event.preventDefault();
+ }
+ }
+ });
+
+ scope.$on(BROADCAST_NEW_HISTORY_ITEM, function(ngEvent, item) {
+ history.push(item);
+ });
+
+ history.init(element, scope.roomId);
+ },
+
}
}])
diff --git a/syweb/webclient/room/room.html b/syweb/webclient/room/room.html
new file mode 100644
index 0000000000..17565f879b
--- /dev/null
+++ b/syweb/webclient/room/room.html
@@ -0,0 +1,266 @@
+<div ng-controller="RoomController" data-ng-init="onInit()" class="room" style="height: 100%;">
+
+ <script type="text/ng-template" id="eventInfoTemplate.html">
+ <div class="modal-body">
+ <pre> {{event_selected | json}} </pre>
+ </div>
+ <div class="modal-footer">
+ <button ng-click="redact()" type="button" class="btn btn-danger redact-button"
+ ng-disabled="!room.current_room_state.state('m.room.ops_levels').content.redact_level || !pow(room_id, state.user_id) || pow(room_id, state.user_id) < room.current_room_state.state('m.room.ops_levels').content.redact_level"
+ title="Delete this event on all home servers. This cannot be undone.">
+ Redact
+ </button>
+
+ <button ng-click="dismiss()" type="button" class="btn">
+ Close
+ </button>
+ </div>
+ </script>
+
+ <script type="text/ng-template" id="roomInfoTemplate.html">
+ <div class="modal-body">
+ <span>
+ Invite a user:
+ <input ng-model="userIDToInvite" size="32" type="text" ng-enter="inviteUser()" ng-disabled="state.permission_denied" placeholder="User ID (ex:@user:homeserver)"/>
+ <button ng-click="inviteUser()" ng-disabled="state.permission_denied">Invite</button>
+ </span>
+ <br/>
+ <br/>
+ <button ng-click="leaveRoom()" ng-disabled="state.permission_denied">Leave Room</button>
+ </br/>
+ <table class="room-info">
+ <tr ng-repeat="(key, event) in roomInfo.stateEvents" class="room-info-event">
+ <td class="room-info-event-meta" width="30%">
+ <span class="monospace">{{ event.type }}</span>
+ <span ng-show="event.state_key" class="monospace"> ({{event.state_key}})</span>
+ <br/>
+ {{ (event.origin_server_ts) | date:'MMM d HH:mm' }}
+ <br/>
+ Set by: <span class="monospace">{{ event.user_id }}</span>
+ <br/>
+ <span ng-show="event.required_power_level >= 0">Required power level: {{event.required_power_level}}<br/></span>
+ <button ng-click="submit(event)" type="button" class="btn btn-success" ng-disabled="!event.content">
+ Submit
+ </button>
+ </td>
+ <td class="room-info-event-content" width="70%">
+ <textarea class="room-info-textarea-content" msd-elastic ng-model="event.content" asjson></textarea>
+ </td>
+ </tr>
+ <tr>
+ <td class="room-info-event-meta" width="30%">
+ <input ng-model="roomInfo.newEvent.type" placeholder="your.event.type" />
+ <br/>
+ <button ng-click="submit(roomInfo.newEvent)" type="button" class="btn btn-success" ng-disabled="!roomInfo.newEvent.content || !roomInfo.newEvent.type">
+ Submit
+ </button>
+ </td>
+ <td class="room-info-event-content" width="70%">
+ <textarea class="room-info-textarea-content" msd-elastic ng-model="roomInfo.newEvent.content" asjson></textarea>
+ </td>
+ </tr>
+ </table>
+ </div>
+ <div class="modal-footer">
+ <button ng-click="dismiss()" type="button" class="btn">
+ Close
+ </button>
+ </div>
+ </script>
+
+ <div id="roomHeader">
+ <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
+
+ <div id="controlButtons">
+ <button ng-click="startVoiceCall()" class="controlButton"
+ style="background: url('img/voice.png')"
+ ng-show="(currentCall == undefined || currentCall.state == 'ended')"
+ ng-disabled="state.permission_denied"
+ >
+ </button>
+ <button ng-click="startVideoCall()" class="controlButton"
+ style="background: url('img/video.png')"
+ ng-show="(currentCall == undefined || currentCall.state == 'ended')"
+ ng-disabled="state.permission_denied"
+ >
+ </button>
+ <button ng-click="openRoomInfo()" class="controlButton"
+ style="background: url('img/settings.png')"
+ >
+ </button>
+ </div>
+
+ <div class="roomHeaderInfo">
+
+ <div class="roomNameSection">
+ <div ng-hide="name.isEditing" ng-dblclick="name.editName()" id="roomName">
+ {{ 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" placeholder="Room name"/>
+ </form>
+ </div>
+
+ <div class="roomTopicSection">
+ <button ng-hide="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing"
+ ng-click="topic.editTopic()" class="roomTopicSetNew">
+ Set Topic
+ </button>
+ <div ng-show="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing">
+ <div ng-hide="topic.isEditing" ng-dblclick="topic.editTopic()" id="roomTopic"
+ ng-bind-html="room.current_room_state.state_events['m.room.topic'].content.topic | limitTo: 200 | linky:'_blank'">
+ </div>
+ <form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
+ <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" placeholder="Topic"/>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="roomPage">
+ <div id="roomWrapper">
+
+ <div id="roomRecentsTableWrapper">
+ <div ng-include="'recents/recents.html'"></div>
+ </div>
+
+ <div id="usersTableWrapper" ng-hide="state.permission_denied">
+ <div ng-repeat="member in members | orderMembersList" class="userAvatar">
+ <div class="userAvatarFrame" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
+ <img class="userAvatarImage mouse-pointer"
+ ng-click="$parent.goToUserPage(member.id)"
+ ng-src="{{member.avatar_url || 'img/default-profile.png'}}"
+ alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
+ title="{{ member.id }} - power: {{ member.powerLevel }}"
+ width="80" height="80"/>
+ <!-- <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div> -->
+ </div>
+ <div class="userName">
+ <pie-chart ng-show="member.powerLevelNorm" data="[ (member.powerLevelNorm + 0), (100 - member.powerLevelNorm) ]"></pie-chart>
+ {{ member.id | mUserDisplayName:room_id:true }}
+ <span ng-show="member.last_active_ago" style="color: #aaa">({{ member.last_active_ago + (now - member.last_updated) | duration }})</span>
+ </div>
+ </div>
+ </div>
+
+ <div id="messageTableWrapper"
+ ng-hide="state.permission_denied"
+ ng-style="{ 'visibility': state.messages_visibility }"
+ keep-scroll>
+ <table id="messageTable" infinite-scroll="paginateMore()">
+ <tr ng-repeat="msg in room.events"
+ ng-class="(room.events[$index - 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
+ <td class="leftBlock" ng-mouseover="state.showTs = 1" ng-mouseout="state.showTs = 0">
+ <div class="timestamp"
+ ng-style="{ 'opacity': state.showTs ? 1.0 : 0.0 }"
+ ng-class="msg.echo_msg_state">
+ {{ (msg.origin_server_ts) | date:'MMM d HH:mm' }}
+ </div>
+ <div class="sender" ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"> {{ msg.__room_member.cnt.displayname || msg.user_id | mUserDisplayName:room_id:true }}</div>
+ </td>
+ <td class="avatar">
+ <!-- msg.__room_member.avatar_url is just backwards compat, and can be removed in the future. -->
+ <img class="avatarImage" ng-src="{{ msg.__room_member.cnt.avatar_url || msg.__room_member.avatar_url || 'img/default-profile.png' }}" width="32" height="32" title="{{msg.user_id}}"
+ ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
+ </td>
+ <td class="msg" ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
+ <div class="bubble" ng-dblclick="openJson(msg)">
+ <span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'">
+ {{ msg.content.displayname || members[msg.state_key].displayname || msg.state_key }} joined
+ </span>
+ <span ng-if="'leave' === msg.content.membership && msg.changedKey === 'membership'">
+ <span ng-if="msg.user_id === msg.state_key">
+ <!-- FIXME: This seems like a synapse bug that the 'leave' content doesn't give the displayname... -->
+ {{ msg.__room_member.cnt.displayname || members[msg.state_key].displayname || msg.state_key }} left
+ </span>
+ <span ng-if="msg.user_id !== msg.state_key && msg.prev_content">
+ {{ msg.content.displayname || members[msg.user_id].displayname || msg.user_id }}
+ {{ {"invite": "kicked", "join": "kicked", "ban": "unbanned"}[msg.prev_content.membership] }}
+ {{ msg.__target_room_member.content.displayname || msg.state_key }}
+ <span ng-if="'join' === msg.prev_content.membership && msg.content.reason">
+ : {{ msg.content.reason }}
+ </span>
+ </span>
+ </span>
+ <span ng-if="'invite' === msg.content.membership && msg.changedKey === 'membership' ||
+ 'ban' === msg.content.membership && msg.changedKey === 'membership'">
+ {{ msg.__room_member.cnt.displayname || msg.user_id }}
+ {{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
+ {{ msg.__target_room_member.cnt.displayname || msg.state_key }}
+ <span ng-if="msg.prev_content && 'ban' === msg.prev_content.membership && msg.content.reason">
+ : {{ msg.content.reason }}
+ </span>
+ </span>
+ <span ng-if="msg.changedKey === 'displayname'">
+ {{ msg.user_id }} changed their display name from {{ msg.prev_content.displayname }} to {{ msg.content.displayname }}
+ </span>
+
+ <span ng-show='msg.content.msgtype === "m.emote"'
+ ng-class="msg.echo_msg_state"
+ ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"
+ />
+
+ <span ng-show='msg.content.msgtype === "m.text"'
+ class="message"
+ ng-class="containsBingWord(msg) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
+ ng-bind-html="(msg.content.msgtype === 'm.text' && msg.type === 'm.room.message' && msg.content.format === 'org.matrix.custom.html') ?
+ (msg.content.formatted_body | unsanitizedLinky) :
+ (msg.content.msgtype === 'm.text' && msg.type === 'm.room.message') ? (msg.content.body | linky:'_blank') : '' "/>
+
+ <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call{{ isWebRTCSupported() ? '' : ' (But your browser does not support VoIP)' }}</span>
+ <span ng-show='msg.type === "m.call.invite" && msg.user_id != state.user_id'>Incoming Call{{ isWebRTCSupported() ? '' : ' (But your browser does not support VoIP)' }}</span>
+
+ <div ng-show='msg.content.msgtype === "m.image"'>
+ <div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
+ <img class="image" ng-src="{{ msg.content.url }}"/>
+ </div>
+ <div ng-show='msg.content.thumbnail_url' ng-style="{ 'height' : msg.content.thumbnail_info.h }">
+ <img class="image mouse-pointer" ng-src="{{ msg.content.thumbnail_url }}"
+ ng-click="$parent.fullScreenImageURL = msg.content.url; $event.stopPropagation();"/>
+ </div>
+ </div>
+
+ <span ng-if="'m.room.topic' === msg.type">
+ {{ members[msg.user_id].displayname || msg.user_id }} changed the topic to: {{ msg.content.topic }}
+ </span>
+
+ <span ng-if="'m.room.name' === msg.type">
+ {{ members[msg.user_id].displayname || msg.user_id }} changed the room name to: {{ msg.content.name }}
+ </span>
+
+ </div>
+ </td>
+ <td class="rightBlock">
+ <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32"
+ ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <div ng-show="state.permission_denied">
+ {{ state.permission_denied }}
+ </div>
+
+ </div>
+ </div>
+
+ <div id="controlPanel">
+ <div id="controls">
+ <button id="attachButton" m-file-input="imageFileToSend" class="extraControls" ng-disabled="state.permission_denied"></button>
+ <textarea id="mainInput" rows="1" ng-enter="send()"
+ ng-disabled="state.permission_denied"
+ ng-focus="true" autocomplete="off" tab-complete command-history="room_id"/>
+ {{ feedback }}
+ <div ng-show="state.stream_failure">
+ {{ state.stream_failure.data.error || "Connection failure" }}
+ </div>
+ </div>
+ </div>
+
+ <div id="room-fullscreen-image" ng-show="fullScreenImageURL" ng-click="fullScreenImageURL = undefined;">
+ <img ng-src="{{ fullScreenImageURL }}"/>
+ </div>
+
+ </div>
diff --git a/webclient/settings/settings-controller.js b/syweb/webclient/settings/settings-controller.js
index 9cdace704a..9cdace704a 100644
--- a/webclient/settings/settings-controller.js
+++ b/syweb/webclient/settings/settings-controller.js
diff --git a/webclient/settings/settings.html b/syweb/webclient/settings/settings.html
index 094c846f8b..094c846f8b 100644
--- a/webclient/settings/settings.html
+++ b/syweb/webclient/settings/settings.html
diff --git a/webclient/test/README b/syweb/webclient/test/README
index 1a7bc832c7..e7ed4eaa87 100644
--- a/webclient/test/README
+++ b/syweb/webclient/test/README
@@ -1,13 +1,31 @@
-Requires:
- - nodejs/npm
- - npm install karma
+Testing is done using Karma.
+
+
+UNIT TESTING
+============
+
+Requires the following:
+ - npm/nodejs
+ - phantomjs
+
+Requires the following node packages:
- npm install jasmine
- - npm install protractor (e2e testing)
+ - npm install karma
+ - npm install karma-jasmine
+ - npm install karma-phantomjs-launcher
+ - npm install karma-junit-reporter
-Setting up continuous integration / run the unit tests (make sure you're in
-this directory so it can find the config file):
+Make sure you're in this directory so it can find the config file and run:
karma start
+You should see all the tests pass.
+
+
+E2E TESTING
+===========
+
+npm install protractor
+
Setting up e2e tests (only if you don't have a selenium server to run the tests
on. If you do, edit the config to point to that url):
diff --git a/webclient/test/e2e/home.spec.js b/syweb/webclient/test/e2e/home.spec.js
index 470237d557..470237d557 100644
--- a/webclient/test/e2e/home.spec.js
+++ b/syweb/webclient/test/e2e/home.spec.js
diff --git a/webclient/test/karma.conf.js b/syweb/webclient/test/karma.conf.js
index 22c4eaaafa..37a9eaf1c1 100644
--- a/webclient/test/karma.conf.js
+++ b/syweb/webclient/test/karma.conf.js
@@ -22,19 +22,27 @@ module.exports = function(config) {
'../js/angular-route.js',
'../js/angular-animate.js',
'../js/angular-sanitize.js',
+ '../js/jquery.peity.min.js',
+ '../js/angular-peity.js',
'../js/ng-infinite-scroll-matrix.js',
- '../login/**/*.*',
- '../room/**/*.*',
- '../components/**/*.*',
- '../user/**/*.*',
- '../home/**/*.*',
- '../recents/**/*.*',
- '../settings/**/*.*',
+ '../js/ui-bootstrap*',
+ '../js/elastic.js',
+ '../login/**/*.js',
+ '../room/**/*.js',
+ '../components/**/*.js',
+ '../user/**/*.js',
+ '../home/**/*.js',
+ '../recents/**/*.js',
+ '../settings/**/*.js',
'../app.js',
'../app*',
'./unit/**/*.js'
],
+ plugins: [
+ 'karma-*',
+ ],
+
// list of files to exclude
exclude: [
@@ -44,14 +52,31 @@ module.exports = function(config) {
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
+ '../login/**/*.js': 'coverage',
+ '../room/**/*.js': 'coverage',
+ '../components/**/*.js': 'coverage',
+ '../user/**/*.js': 'coverage',
+ '../home/**/*.js': 'coverage',
+ '../recents/**/*.js': 'coverage',
+ '../settings/**/*.js': 'coverage',
+ '../app.js': 'coverage'
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
- reporters: ['progress'],
+ reporters: ['progress', 'junit', 'coverage'],
+ junitReporter: {
+ outputFile: 'test-results.xml',
+ suite: ''
+ },
+ coverageReporter: {
+ type: 'cobertura',
+ dir: 'coverage/',
+ file: 'coverage.xml'
+ },
// web server port
port: 9876,
@@ -72,11 +97,11 @@ module.exports = function(config) {
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
- browsers: ['Chrome'],
+ browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
- singleRun: false
+ singleRun: true
});
};
diff --git a/webclient/test/protractor.conf.js b/syweb/webclient/test/protractor.conf.js
index 76ae7b712b..76ae7b712b 100644
--- a/webclient/test/protractor.conf.js
+++ b/syweb/webclient/test/protractor.conf.js
diff --git a/syweb/webclient/test/unit/commands-service.spec.js b/syweb/webclient/test/unit/commands-service.spec.js
new file mode 100644
index 0000000000..142044f153
--- /dev/null
+++ b/syweb/webclient/test/unit/commands-service.spec.js
@@ -0,0 +1,143 @@
+describe('CommandsService', function() {
+ var scope;
+ var roomId = "!dlwifhweu:localhost";
+
+ var testPowerLevelsEvent, testMatrixServicePromise;
+
+ var matrixService = { // these will be spyed on by jasmine, hence stub methods
+ setDisplayName: function(args){},
+ kick: function(args){},
+ ban: function(args){},
+ unban: function(args){},
+ setUserPowerLevel: function(args){}
+ };
+
+ var modelService = {
+ getRoom: function(roomId) {
+ return {
+ room_id: roomId,
+ current_room_state: {
+ events: {
+ "m.room.power_levels": testPowerLevelsEvent
+ },
+ state: function(type, key) {
+ return key ? this.events[type+key] : this.events[type];
+ }
+ }
+ };
+ }
+ };
+
+
+ // helper function for asserting promise outcomes
+ NOTHING = "[Promise]";
+ RESOLVED = "[Resolved promise]";
+ REJECTED = "[Rejected promise]";
+ var expectPromise = function(promise, expects) {
+ var value = NOTHING;
+ promise.then(function(result) {
+ value = RESOLVED;
+ }, function(fail) {
+ value = REJECTED;
+ });
+ scope.$apply();
+ expect(value).toEqual(expects);
+ };
+
+ // setup the service and mocked dependencies
+ beforeEach(function() {
+
+ // set default mock values
+ testPowerLevelsEvent = {
+ content: {
+ default: 50
+ },
+ user_id: "@foo:bar",
+ room_id: roomId
+ }
+
+ // mocked dependencies
+ module(function ($provide) {
+ $provide.value('matrixService', matrixService);
+ $provide.value('modelService', modelService);
+ });
+
+ // tested service
+ module('commandsService');
+ });
+
+ beforeEach(inject(function($rootScope, $q) {
+ scope = $rootScope;
+ testMatrixServicePromise = $q.defer();
+ }));
+
+ it('should reject a no-arg "/nick".', inject(
+ function(commandsService) {
+ var promise = commandsService.processInput(roomId, "/nick");
+ expectPromise(promise, REJECTED);
+ }));
+
+ it('should be able to set a /nick with multiple words.', inject(
+ function(commandsService) {
+ spyOn(matrixService, 'setDisplayName').and.returnValue(testMatrixServicePromise);
+ var promise = commandsService.processInput(roomId, "/nick Bob Smith");
+ expect(matrixService.setDisplayName).toHaveBeenCalledWith("Bob Smith");
+ expect(promise).toBe(testMatrixServicePromise);
+ }));
+
+ it('should be able to /kick a user without a reason.', inject(
+ function(commandsService) {
+ spyOn(matrixService, 'kick').and.returnValue(testMatrixServicePromise);
+ var promise = commandsService.processInput(roomId, "/kick @bob:matrix.org");
+ expect(matrixService.kick).toHaveBeenCalledWith(roomId, "@bob:matrix.org", undefined);
+ expect(promise).toBe(testMatrixServicePromise);
+ }));
+
+ it('should be able to /kick a user with a reason.', inject(
+ function(commandsService) {
+ spyOn(matrixService, 'kick').and.returnValue(testMatrixServicePromise);
+ var promise = commandsService.processInput(roomId, "/kick @bob:matrix.org he smells");
+ expect(matrixService.kick).toHaveBeenCalledWith(roomId, "@bob:matrix.org", "he smells");
+ expect(promise).toBe(testMatrixServicePromise);
+ }));
+
+ it('should be able to /ban a user without a reason.', inject(
+ function(commandsService) {
+ spyOn(matrixService, 'ban').and.returnValue(testMatrixServicePromise);
+ var promise = commandsService.processInput(roomId, "/ban @bob:matrix.org");
+ expect(matrixService.ban).toHaveBeenCalledWith(roomId, "@bob:matrix.org", undefined);
+ expect(promise).toBe(testMatrixServicePromise);
+ }));
+
+ it('should be able to /ban a user with a reason.', inject(
+ function(commandsService) {
+ spyOn(matrixService, 'ban').and.returnValue(testMatrixServicePromise);
+ var promise = commandsService.processInput(roomId, "/ban @bob:matrix.org he smells");
+ expect(matrixService.ban).toHaveBeenCalledWith(roomId, "@bob:matrix.org", "he smells");
+ expect(promise).toBe(testMatrixServicePromise);
+ }));
+
+ it('should be able to /unban a user.', inject(
+ function(commandsService) {
+ spyOn(matrixService, 'unban').and.returnValue(testMatrixServicePromise);
+ var promise = commandsService.processInput(roomId, "/unban @bob:matrix.org");
+ expect(matrixService.unban).toHaveBeenCalledWith(roomId, "@bob:matrix.org");
+ expect(promise).toBe(testMatrixServicePromise);
+ }));
+
+ it('should be able to /op a user.', inject(
+ function(commandsService) {
+ spyOn(matrixService, 'setUserPowerLevel').and.returnValue(testMatrixServicePromise);
+ var promise = commandsService.processInput(roomId, "/op @bob:matrix.org 50");
+ expect(matrixService.setUserPowerLevel).toHaveBeenCalledWith(roomId, "@bob:matrix.org", 50, testPowerLevelsEvent);
+ expect(promise).toBe(testMatrixServicePromise);
+ }));
+
+ it('should be able to /deop a user.', inject(
+ function(commandsService) {
+ spyOn(matrixService, 'setUserPowerLevel').and.returnValue(testMatrixServicePromise);
+ var promise = commandsService.processInput(roomId, "/deop @bob:matrix.org");
+ expect(matrixService.setUserPowerLevel).toHaveBeenCalledWith(roomId, "@bob:matrix.org", undefined, testPowerLevelsEvent);
+ expect(promise).toBe(testMatrixServicePromise);
+ }));
+});
diff --git a/syweb/webclient/test/unit/event-handler-service.spec.js b/syweb/webclient/test/unit/event-handler-service.spec.js
new file mode 100644
index 0000000000..2a4dc3b5a5
--- /dev/null
+++ b/syweb/webclient/test/unit/event-handler-service.spec.js
@@ -0,0 +1,117 @@
+describe('EventHandlerService', function() {
+ var scope;
+
+ var modelService = {};
+
+ // setup the service and mocked dependencies
+ beforeEach(function() {
+ // dependencies
+ module('matrixService');
+ module('notificationService');
+ module('mPresence');
+
+ // cleanup mocked methods
+ modelService = {};
+
+ // mocked dependencies
+ module(function ($provide) {
+ $provide.value('modelService', modelService);
+ });
+
+ // tested service
+ module('eventHandlerService');
+ });
+
+ beforeEach(inject(function($rootScope) {
+ scope = $rootScope;
+ }));
+
+ it('should be able to get the number of joined users in a room', inject(
+ function(eventHandlerService) {
+ var roomId = "!foo:matrix.org";
+ // set mocked data
+ modelService.getRoom = function(roomId) {
+ return {
+ room_id: roomId,
+ current_room_state: {
+ members: {
+ "@adam:matrix.org": {
+ event: {
+ content: { membership: "join" },
+ user_id: "@adam:matrix.org"
+ }
+ },
+ "@beth:matrix.org": {
+ event: {
+ content: { membership: "invite" },
+ user_id: "@beth:matrix.org"
+ }
+ },
+ "@charlie:matrix.org": {
+ event: {
+ content: { membership: "join" },
+ user_id: "@charlie:matrix.org"
+ }
+ },
+ "@danice:matrix.org": {
+ event: {
+ content: { membership: "leave" },
+ user_id: "@danice:matrix.org"
+ }
+ }
+ }
+ }
+ };
+ }
+
+ var num = eventHandlerService.getUsersCountInRoom(roomId);
+ expect(num).toEqual(2);
+ }));
+
+ it('should be able to get a users power level', inject(
+ function(eventHandlerService) {
+ var roomId = "!foo:matrix.org";
+ // set mocked data
+ modelService.getRoom = function(roomId) {
+ return {
+ room_id: roomId,
+ current_room_state: {
+ members: {
+ "@adam:matrix.org": {
+ event: {
+ content: { membership: "join" },
+ user_id: "@adam:matrix.org"
+ }
+ },
+ "@beth:matrix.org": {
+ event: {
+ content: { membership: "join" },
+ user_id: "@beth:matrix.org"
+ }
+ }
+ },
+ s: {
+ "m.room.power_levels": {
+ content: {
+ "@adam:matrix.org": 90,
+ "default": 50
+ }
+ }
+ },
+ state: function(type, key) {
+ return key ? this.s[type+key] : this.s[type]
+ }
+ }
+ };
+ };
+
+ var num = eventHandlerService.getUserPowerLevel(roomId, "@beth:matrix.org");
+ expect(num).toEqual(50);
+
+ num = eventHandlerService.getUserPowerLevel(roomId, "@adam:matrix.org");
+ expect(num).toEqual(90);
+
+ num = eventHandlerService.getUserPowerLevel(roomId, "@unknown:matrix.org");
+ expect(num).toEqual(50);
+ }));
+});
diff --git a/syweb/webclient/test/unit/filters.spec.js b/syweb/webclient/test/unit/filters.spec.js
new file mode 100644
index 0000000000..c6253aad96
--- /dev/null
+++ b/syweb/webclient/test/unit/filters.spec.js
@@ -0,0 +1,635 @@
+describe('mRoomName filter', function() {
+ var filter, mRoomName, mUserDisplayName;
+
+ var roomId = "!weufhewifu:matrix.org";
+
+ // test state values (f.e. test)
+ var testUserId, testAlias, testDisplayName, testOtherDisplayName, testRoomState;
+
+ // mocked services which return the test values above.
+ var matrixService = {
+ config: function() {
+ return {
+ user_id: testUserId
+ };
+ }
+ };
+
+ var modelService = {
+ getRoom: function(room_id) {
+ return {
+ current_room_state: testRoomState
+ };
+ },
+
+ getRoomIdToAliasMapping: function(room_id) {
+ return testAlias;
+ },
+ };
+
+ beforeEach(function() {
+ // inject mocked dependencies
+ module(function ($provide) {
+ $provide.value('matrixService', matrixService);
+ $provide.value('modelService', modelService);
+ });
+
+ module('matrixFilter');
+
+ // angular resolves dependencies with the same name via a 'last wins'
+ // rule, hence we need to have this mock filter impl AFTER module('matrixFilter')
+ // so it clobbers the actual mUserDisplayName implementation.
+ module(function ($filterProvider) {
+ // provide a fake filter
+ $filterProvider.register('mUserDisplayName', function() {
+ return function(user_id, room_id) {
+ if (user_id === testUserId) {
+ return testDisplayName;
+ }
+ return testOtherDisplayName;
+ };
+ });
+ });
+ });
+
+
+ beforeEach(inject(function($filter) {
+ filter = $filter;
+ mRoomName = filter("mRoomName");
+
+ // purge the previous test values
+ testUserId = undefined;
+ testAlias = undefined;
+ testDisplayName = undefined;
+ testOtherDisplayName = undefined;
+
+ // mock up a stub room state
+ testRoomState = {
+ s:{}, // internal; stores the state events
+ state: function(type, key) {
+ // accessor used by filter
+ return key ? this.s[type+key] : this.s[type];
+ },
+ members: {}, // struct used by filter
+
+ // test helper methods
+ setJoinRule: function(rule) {
+ this.s["m.room.join_rules"] = {
+ content: {
+ join_rule: rule
+ }
+ };
+ },
+ setRoomName: function(name) {
+ this.s["m.room.name"] = {
+ content: {
+ name: name
+ }
+ };
+ },
+ setMember: function(user_id, membership, inviter_user_id) {
+ if (!inviter_user_id) {
+ inviter_user_id = user_id;
+ }
+ this.s["m.room.member" + user_id] = {
+ event: {
+ content: {
+ membership: membership
+ },
+ state_key: user_id,
+ user_id: inviter_user_id
+ }
+ };
+ this.members[user_id] = this.s["m.room.member" + user_id];
+ }
+ };
+ }));
+
+ /**** ROOM NAME ****/
+
+ it("should show the room name if one exists for private (invite join_rules) rooms.", function() {
+ var roomName = "The Room Name";
+ testUserId = "@me:matrix.org";
+ testRoomState.setJoinRule("invite");
+ testRoomState.setRoomName(roomName);
+ testRoomState.setMember(testUserId, "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(roomName);
+ });
+
+ it("should show the room name if one exists for public (public join_rules) rooms.", function() {
+ var roomName = "The Room Name";
+ testUserId = "@me:matrix.org";
+ testRoomState.setJoinRule("public");
+ testRoomState.setRoomName(roomName);
+ testRoomState.setMember(testUserId, "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(roomName);
+ });
+
+ /**** ROOM ALIAS ****/
+
+ it("should show the room alias if one exists for private (invite join_rules) rooms if a room name doesn't exist.", function() {
+ testAlias = "#thealias:matrix.org";
+ testUserId = "@me:matrix.org";
+ testRoomState.setJoinRule("invite");
+ testRoomState.setMember(testUserId, "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(testAlias);
+ });
+
+ it("should show the room alias if one exists for public (public join_rules) rooms if a room name doesn't exist.", function() {
+ testAlias = "#thealias:matrix.org";
+ testUserId = "@me:matrix.org";
+ testRoomState.setJoinRule("public");
+ testRoomState.setMember(testUserId, "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(testAlias);
+ });
+
+ /**** ROOM ID ****/
+
+ it("should show the room ID for public (public join_rules) rooms if a room name and alias don't exist.", function() {
+ testUserId = "@me:matrix.org";
+ testRoomState.setJoinRule("public");
+ testRoomState.setMember(testUserId, "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(roomId);
+ });
+
+ it("should show the room ID for private (invite join_rules) rooms if a room name and alias don't exist and there are >2 members.", function() {
+ testUserId = "@me:matrix.org";
+ testRoomState.setJoinRule("public");
+ testRoomState.setMember(testUserId, "join");
+ testRoomState.setMember("@alice:matrix.org", "join");
+ testRoomState.setMember("@bob:matrix.org", "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(roomId);
+ });
+
+ /**** SELF-CHAT ****/
+
+ it("should show your display name for private (invite join_rules) rooms if a room name and alias don't exist and it is a self-chat.", function() {
+ testUserId = "@me:matrix.org";
+ testDisplayName = "Me";
+ testRoomState.setJoinRule("private");
+ testRoomState.setMember(testUserId, "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(testDisplayName);
+ });
+
+ it("should show your user ID for private (invite join_rules) rooms if a room name and alias don't exist and it is a self-chat and they don't have a display name set.", function() {
+ testUserId = "@me:matrix.org";
+ testRoomState.setJoinRule("private");
+ testRoomState.setMember(testUserId, "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(testUserId);
+ });
+
+ /**** ONE-TO-ONE CHAT ****/
+
+ it("should show the other user's display name for private (invite join_rules) rooms if a room name and alias don't exist and it is a 1:1-chat.", function() {
+ testUserId = "@me:matrix.org";
+ otherUserId = "@alice:matrix.org";
+ testOtherDisplayName = "Alice";
+ testRoomState.setJoinRule("private");
+ testRoomState.setMember(testUserId, "join");
+ testRoomState.setMember("@alice:matrix.org", "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(testOtherDisplayName);
+ });
+
+ it("should show the other user's ID for private (invite join_rules) rooms if a room name and alias don't exist and it is a 1:1-chat and they don't have a display name set.", function() {
+ testUserId = "@me:matrix.org";
+ otherUserId = "@alice:matrix.org";
+ testRoomState.setJoinRule("private");
+ testRoomState.setMember(testUserId, "join");
+ testRoomState.setMember("@alice:matrix.org", "join");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(otherUserId);
+ });
+
+ /**** INVITED TO ROOM ****/
+
+ it("should show the other user's display name for private (invite join_rules) rooms if you are invited to it.", function() {
+ testUserId = "@me:matrix.org";
+ testDisplayName = "Me";
+ otherUserId = "@alice:matrix.org";
+ testOtherDisplayName = "Alice";
+ testRoomState.setJoinRule("private");
+ testRoomState.setMember(testUserId, "join");
+ testRoomState.setMember(otherUserId, "join");
+ testRoomState.setMember(testUserId, "invite");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(testOtherDisplayName);
+ });
+
+ it("should show the other user's ID for private (invite join_rules) rooms if you are invited to it and the inviter doesn't have a display name.", function() {
+ testUserId = "@me:matrix.org";
+ testDisplayName = "Me";
+ otherUserId = "@alice:matrix.org";
+ testRoomState.setJoinRule("private");
+ testRoomState.setMember(testUserId, "join");
+ testRoomState.setMember(otherUserId, "join");
+ testRoomState.setMember(testUserId, "invite");
+ var output = mRoomName(roomId);
+ expect(output).toEqual(otherUserId);
+ });
+});
+
+describe('duration filter', function() {
+ var filter, durationFilter;
+
+ beforeEach(module('matrixWebClient'));
+ beforeEach(inject(function($filter) {
+ filter = $filter;
+ durationFilter = filter("duration");
+ }));
+
+ it("should represent 15000 ms as '15s'", function() {
+ var output = durationFilter(15000);
+ expect(output).toEqual("15s");
+ });
+
+ it("should represent 60000 ms as '1m'", function() {
+ var output = durationFilter(60000);
+ expect(output).toEqual("1m");
+ });
+
+ it("should represent 65000 ms as '1m'", function() {
+ var output = durationFilter(65000);
+ expect(output).toEqual("1m");
+ });
+
+ it("should represent 10 ms as '0s'", function() {
+ var output = durationFilter(10);
+ expect(output).toEqual("0s");
+ });
+
+ it("should represent 4m as '4m'", function() {
+ var output = durationFilter(1000*60*4);
+ expect(output).toEqual("4m");
+ });
+
+ it("should represent 4m30s as '4m'", function() {
+ var output = durationFilter(1000*60*4 + 1000*30);
+ expect(output).toEqual("4m");
+ });
+
+ it("should represent 2h as '2h'", function() {
+ var output = durationFilter(1000*60*60*2);
+ expect(output).toEqual("2h");
+ });
+
+ it("should represent 2h35m as '2h'", function() {
+ var output = durationFilter(1000*60*60*2 + 1000*60*35);
+ expect(output).toEqual("2h");
+ });
+});
+
+describe('orderMembersList filter', function() {
+ var filter, orderMembersList;
+
+ beforeEach(module('matrixWebClient'));
+ beforeEach(inject(function($filter) {
+ filter = $filter;
+ orderMembersList = filter("orderMembersList");
+ }));
+
+ it("should sort a single entry", function() {
+ var output = orderMembersList({
+ "@a:example.com": {
+ last_active_ago: 50,
+ last_updated: 1415266943964
+ }
+ });
+ expect(output).toEqual([{
+ id: "@a:example.com",
+ last_active_ago: 50,
+ last_updated: 1415266943964
+ }]);
+ });
+
+ it("should sort by taking last_active_ago into account", function() {
+ var output = orderMembersList({
+ "@a:example.com": {
+ last_active_ago: 1000,
+ last_updated: 1415266943964
+ },
+ "@b:example.com": {
+ last_active_ago: 50,
+ last_updated: 1415266943964
+ },
+ "@c:example.com": {
+ last_active_ago: 99999,
+ last_updated: 1415266943964
+ }
+ });
+ expect(output).toEqual([
+ {
+ id: "@b:example.com",
+ last_active_ago: 50,
+ last_updated: 1415266943964
+ },
+ {
+ id: "@a:example.com",
+ last_active_ago: 1000,
+ last_updated: 1415266943964
+ },
+ {
+ id: "@c:example.com",
+ last_active_ago: 99999,
+ last_updated: 1415266943964
+ },
+ ]);
+ });
+
+ it("should sort by taking last_updated into account", function() {
+ var output = orderMembersList({
+ "@a:example.com": {
+ last_active_ago: 1000,
+ last_updated: 1415266943964
+ },
+ "@b:example.com": {
+ last_active_ago: 1000,
+ last_updated: 1415266900000
+ },
+ "@c:example.com": {
+ last_active_ago: 1000,
+ last_updated: 1415266943000
+ }
+ });
+ expect(output).toEqual([
+ {
+ id: "@a:example.com",
+ last_active_ago: 1000,
+ last_updated: 1415266943964
+ },
+ {
+ id: "@c:example.com",
+ last_active_ago: 1000,
+ last_updated: 1415266943000
+ },
+ {
+ id: "@b:example.com",
+ last_active_ago: 1000,
+ last_updated: 1415266900000
+ },
+ ]);
+ });
+
+ it("should sort by taking last_updated and last_active_ago into account",
+ function() {
+ var output = orderMembersList({
+ "@a:example.com": {
+ last_active_ago: 1000,
+ last_updated: 1415266943000
+ },
+ "@b:example.com": {
+ last_active_ago: 100000,
+ last_updated: 1415266943900
+ },
+ "@c:example.com": {
+ last_active_ago: 1000,
+ last_updated: 1415266943964
+ }
+ });
+ expect(output).toEqual([
+ {
+ id: "@c:example.com",
+ last_active_ago: 1000,
+ last_updated: 1415266943964
+ },
+ {
+ id: "@a:example.com",
+ last_active_ago: 1000,
+ last_updated: 1415266943000
+ },
+ {
+ id: "@b:example.com",
+ last_active_ago: 100000,
+ last_updated: 1415266943900
+ },
+ ]);
+ });
+
+ // SYWEB-26 comment
+ it("should sort members who do not have last_active_ago value at the end of the list",
+ function() {
+ // single undefined entry
+ var output = orderMembersList({
+ "@a:example.com": {
+ last_active_ago: 1000,
+ last_updated: 1415266943964
+ },
+ "@b:example.com": {
+ last_active_ago: 100000,
+ last_updated: 1415266943964
+ },
+ "@c:example.com": {
+ last_active_ago: undefined,
+ last_updated: 1415266943964
+ }
+ });
+ expect(output).toEqual([
+ {
+ id: "@a:example.com",
+ last_active_ago: 1000,
+ last_updated: 1415266943964
+ },
+ {
+ id: "@b:example.com",
+ last_active_ago: 100000,
+ last_updated: 1415266943964
+ },
+ {
+ id: "@c:example.com",
+ last_active_ago: undefined,
+ last_updated: 1415266943964
+ },
+ ]);
+ });
+
+ it("should sort multiple members who do not have last_active_ago according to presence",
+ function() {
+ // single undefined entry
+ var output = orderMembersList({
+ "@a:example.com": {
+ last_active_ago: undefined,
+ last_updated: 1415266943964,
+ presence: "unavailable"
+ },
+ "@b:example.com": {
+ last_active_ago: undefined,
+ last_updated: 1415266943964,
+ presence: "online"
+ },
+ "@c:example.com": {
+ last_active_ago: undefined,
+ last_updated: 1415266943964,
+ presence: "offline"
+ }
+ });
+ expect(output).toEqual([
+ {
+ id: "@b:example.com",
+ last_active_ago: undefined,
+ last_updated: 1415266943964,
+ presence: "online"
+ },
+ {
+ id: "@a:example.com",
+ last_active_ago: undefined,
+ last_updated: 1415266943964,
+ presence: "unavailable"
+ },
+ {
+ id: "@c:example.com",
+ last_active_ago: undefined,
+ last_updated: 1415266943964,
+ presence: "offline"
+ },
+ ]);
+ });
+});
+describe('mUserDisplayName filter', function() {
+ var filter, mUserDisplayName;
+
+ var roomId = "!weufhewifu:matrix.org";
+
+ // test state values (f.e. test)
+ var testUser_displayname, testUser_user_id;
+ var testSelf_displayname, testSelf_user_id;
+ var testRoomState;
+
+ // mocked services which return the test values above.
+ var matrixService = {
+ config: function() {
+ return {
+ user_id: testSelf_user_id
+ };
+ }
+ };
+
+ var modelService = {
+ getRoom: function(room_id) {
+ return {
+ current_room_state: testRoomState
+ };
+ },
+
+ getUser: function(user_id) {
+ return {
+ event: {
+ content: {
+ displayname: testUser_displayname
+ },
+ event_id: "wfiuhwf@matrix.org",
+ user_id: testUser_user_id
+ }
+ };
+ },
+
+ getMember: function(room_id, user_id) {
+ return testRoomState.members[user_id];
+ }
+ };
+
+ beforeEach(function() {
+ // inject mocked dependencies
+ module(function ($provide) {
+ $provide.value('matrixService', matrixService);
+ $provide.value('modelService', modelService);
+ });
+
+ module('matrixFilter');
+ });
+
+
+ beforeEach(inject(function($filter) {
+ filter = $filter;
+ mUserDisplayName = filter("mUserDisplayName");
+
+ // purge the previous test values
+ testSelf_displayname = "Me";
+ testSelf_user_id = "@me:matrix.org";
+ testUser_displayname = undefined;
+ testUser_user_id = undefined;
+
+ // mock up a stub room state
+ testRoomState = {
+ s:{}, // internal; stores the state events
+ state: function(type, key) {
+ // accessor used by filter
+ return key ? this.s[type+key] : this.s[type];
+ },
+ members: {}, // struct used by filter
+
+ // test helper methods
+ setMember: function(user_id, displayname, membership, inviter_user_id) {
+ if (!inviter_user_id) {
+ inviter_user_id = user_id;
+ }
+ if (!membership) {
+ membership = "join";
+ }
+ this.s["m.room.member" + user_id] = {
+ event: {
+ content: {
+ displayname: displayname,
+ membership: membership
+ },
+ state_key: user_id,
+ user_id: inviter_user_id
+ }
+ };
+ this.members[user_id] = this.s["m.room.member" + user_id];
+ }
+ };
+ }));
+
+ it("should show the display name of a user in a room if they have set one.", function() {
+ testUser_displayname = "Tom Scott";
+ testUser_user_id = "@tymnhk:matrix.org";
+ testRoomState.setMember(testUser_user_id, testUser_displayname);
+ testRoomState.setMember(testSelf_user_id, testSelf_displayname);
+ var output = mUserDisplayName(testUser_user_id, roomId);
+ expect(output).toEqual(testUser_displayname);
+ });
+
+ it("should show the user_id of a user in a room if they have no display name.", function() {
+ testUser_user_id = "@mike:matrix.org";
+ testRoomState.setMember(testUser_user_id, testUser_displayname);
+ testRoomState.setMember(testSelf_user_id, testSelf_displayname);
+ var output = mUserDisplayName(testUser_user_id, roomId);
+ expect(output).toEqual(testUser_user_id);
+ });
+
+ it("should still show the displayname of a user in a room if they are not a member of the room but there exists a User entry for them.", function() {
+ testUser_user_id = "@alice:matrix.org";
+ testUser_displayname = "Alice M";
+ testRoomState.setMember(testSelf_user_id, testSelf_displayname);
+ var output = mUserDisplayName(testUser_user_id, roomId);
+ expect(output).toEqual(testUser_displayname);
+ });
+
+ it("should disambiguate users with the same displayname with their user id.", function() {
+ testUser_displayname = "Reimu";
+ testSelf_displayname = "Reimu";
+ testUser_user_id = "@reimu:matrix.org";
+ testSelf_user_id = "@xreimux:matrix.org";
+ testRoomState.setMember(testUser_user_id, testUser_displayname);
+ testRoomState.setMember(testSelf_user_id, testSelf_displayname);
+ var output = mUserDisplayName(testUser_user_id, roomId);
+ expect(output).toEqual(testUser_displayname + " (" + testUser_user_id + ")");
+ });
+
+ it("should wrap user IDs after the : if the wrap flag is set.", function() {
+ testUser_user_id = "@mike:matrix.org";
+ testRoomState.setMember(testUser_user_id, testUser_displayname);
+ testRoomState.setMember(testSelf_user_id, testSelf_displayname);
+ var output = mUserDisplayName(testUser_user_id, roomId, true);
+ expect(output).toEqual("@mike :matrix.org");
+ });
+});
+
diff --git a/syweb/webclient/test/unit/matrix-service.spec.js b/syweb/webclient/test/unit/matrix-service.spec.js
new file mode 100644
index 0000000000..4959f2395d
--- /dev/null
+++ b/syweb/webclient/test/unit/matrix-service.spec.js
@@ -0,0 +1,504 @@
+describe('MatrixService', function() {
+ var scope, httpBackend;
+ var BASE = "http://example.com";
+ var PREFIX = "/_matrix/client/api/v1";
+ var URL = BASE + PREFIX;
+ var roomId = "!wejigf387t34:matrix.org";
+
+ var CONFIG = {
+ access_token: "foobar",
+ homeserver: BASE
+ };
+
+ beforeEach(module('matrixService'));
+
+ beforeEach(inject(function($rootScope, $httpBackend) {
+ httpBackend = $httpBackend;
+ scope = $rootScope;
+ }));
+
+ afterEach(function() {
+ httpBackend.verifyNoOutstandingExpectation();
+ httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should be able to POST /createRoom with an alias', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var alias = "flibble";
+ matrixService.create(alias).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPOST(URL + "/createRoom?access_token=foobar",
+ {
+ room_alias_name: alias
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to GET /initialSync', inject(function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var limit = 15;
+ matrixService.initialSync(limit).then(function(response) {
+ expect(response.data).toEqual([]);
+ });
+
+ httpBackend.expectGET(
+ URL + "/initialSync?access_token=foobar&limit=15")
+ .respond([]);
+ httpBackend.flush();
+ }));
+
+ it('should be able to GET /rooms/$roomid/state', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ matrixService.roomState(roomId).then(function(response) {
+ expect(response.data).toEqual([]);
+ });
+
+ httpBackend.expectGET(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/state?access_token=foobar")
+ .respond([]);
+ httpBackend.flush();
+ }));
+
+ it('should be able to POST /join', inject(function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ matrixService.joinAlias(roomId).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPOST(
+ URL + "/join/" + encodeURIComponent(roomId) +
+ "?access_token=foobar",
+ {})
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to POST /rooms/$roomid/join', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ matrixService.join(roomId).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPOST(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/join?access_token=foobar",
+ {})
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to POST /rooms/$roomid/invite', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var inviteUserId = "@user:example.com";
+ matrixService.invite(roomId, inviteUserId).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPOST(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/invite?access_token=foobar",
+ {
+ user_id: inviteUserId
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to POST /rooms/$roomid/leave', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ matrixService.leave(roomId).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPOST(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/leave?access_token=foobar",
+ {})
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to POST /rooms/$roomid/ban', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var userId = "@example:example.com";
+ var reason = "Because.";
+ matrixService.ban(roomId, userId, reason).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPOST(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/ban?access_token=foobar",
+ {
+ user_id: userId,
+ reason: reason
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to GET /directory/room/$alias', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var alias = "#test:example.com";
+ var roomId = "!wefuhewfuiw:example.com";
+ matrixService.resolveRoomAlias(alias).then(function(response) {
+ expect(response.data).toEqual({
+ room_id: roomId
+ });
+ });
+
+ httpBackend.expectGET(
+ URL + "/directory/room/" + encodeURIComponent(alias) +
+ "?access_token=foobar")
+ .respond({
+ room_id: roomId
+ });
+ httpBackend.flush();
+ }));
+
+ it('should be able to send m.room.name', inject(function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!fh38hfwfwef:example.com";
+ var name = "Room Name";
+ matrixService.setName(roomId, name).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPUT(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/state/m.room.name?access_token=foobar",
+ {
+ name: name
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to send m.room.topic', inject(function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!fh38hfwfwef:example.com";
+ var topic = "A room topic can go here.";
+ matrixService.setTopic(roomId, topic).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPUT(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/state/m.room.topic?access_token=foobar",
+ {
+ topic: topic
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to send generic state events without a state key', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!fh38hfwfwef:example.com";
+ var eventType = "com.example.events.test";
+ var content = {
+ testing: "1 2 3"
+ };
+ matrixService.sendStateEvent(roomId, eventType, content).then(
+ function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPUT(
+ URL + "/rooms/" + encodeURIComponent(roomId) + "/state/" +
+ encodeURIComponent(eventType) + "?access_token=foobar",
+ content)
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ // TODO: Skipped since the webclient is purposefully broken so as not to
+ // 500 matrix.org
+ xit('should be able to send generic state events with a state key', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!fh38hfwfwef:example.com";
+ var eventType = "com.example.events.test:special@characters";
+ var content = {
+ testing: "1 2 3"
+ };
+ var stateKey = "version:1";
+ matrixService.sendStateEvent(roomId, eventType, content, stateKey).then(
+ function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPUT(
+ URL + "/rooms/" + encodeURIComponent(roomId) + "/state/" +
+ encodeURIComponent(eventType) + "/" + encodeURIComponent(stateKey)+
+ "?access_token=foobar",
+ content)
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to PUT generic events ', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!fh38hfwfwef:example.com";
+ var eventType = "com.example.events.test";
+ var txnId = "42";
+ var content = {
+ testing: "1 2 3"
+ };
+ matrixService.sendEvent(roomId, eventType, txnId, content).then(
+ function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPUT(
+ URL + "/rooms/" + encodeURIComponent(roomId) + "/send/" +
+ encodeURIComponent(eventType) + "/" + encodeURIComponent(txnId)+
+ "?access_token=foobar",
+ content)
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to PUT text messages ', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!fh38hfwfwef:example.com";
+ var body = "ABC 123";
+ matrixService.sendTextMessage(roomId, body).then(
+ function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPUT(
+ new RegExp(URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/send/m.room.message/(.*)" +
+ "?access_token=foobar"),
+ {
+ body: body,
+ msgtype: "m.text"
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to PUT emote messages ', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!fh38hfwfwef:example.com";
+ var body = "ABC 123";
+ matrixService.sendEmoteMessage(roomId, body).then(
+ function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPUT(
+ new RegExp(URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/send/m.room.message/(.*)" +
+ "?access_token=foobar"),
+ {
+ body: body,
+ msgtype: "m.emote"
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to POST redactions', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!fh38hfwfwef:example.com";
+ var eventId = "fwefwexample.com";
+ matrixService.redactEvent(roomId, eventId).then(
+ function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectPOST(URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/redact/" + encodeURIComponent(eventId) +
+ "?access_token=foobar")
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to GET /directory/room/$alias', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var alias = "#test:example.com";
+ var roomId = "!wefuhewfuiw:example.com";
+ matrixService.resolveRoomAlias(alias).then(function(response) {
+ expect(response.data).toEqual({
+ room_id: roomId
+ });
+ });
+
+ httpBackend.expectGET(
+ URL + "/directory/room/" + encodeURIComponent(alias) +
+ "?access_token=foobar")
+ .respond({
+ room_id: roomId
+ });
+ httpBackend.flush();
+ }));
+
+ it('should be able to GET /rooms/$roomid/members', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!wefuhewfuiw:example.com";
+ matrixService.getMemberList(roomId).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectGET(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/members?access_token=foobar")
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to paginate a room', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var roomId = "!wefuhewfuiw:example.com";
+ var from = "3t_44e_54z";
+ var limit = 20;
+ matrixService.paginateBackMessages(roomId, from, limit).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectGET(
+ URL + "/rooms/" + encodeURIComponent(roomId) +
+ "/messages?access_token=foobar&dir=b&from="+
+ encodeURIComponent(from)+"&limit="+limit)
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to GET /publicRooms', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ matrixService.publicRooms().then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectGET(
+ new RegExp(URL + "/publicRooms(.*)"))
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to GET /profile/$userid/displayname', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var userId = "@foo:example.com";
+ matrixService.getDisplayName(userId).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectGET(URL + "/profile/" + encodeURIComponent(userId) +
+ "/displayname?access_token=foobar")
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to GET /profile/$userid/avatar_url', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var userId = "@foo:example.com";
+ matrixService.getProfilePictureUrl(userId).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+
+ httpBackend.expectGET(URL + "/profile/" + encodeURIComponent(userId) +
+ "/avatar_url?access_token=foobar")
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to PUT /profile/$me/avatar_url', inject(
+ function(matrixService) {
+ var testConfig = angular.copy(CONFIG);
+ testConfig.user_id = "@bob:example.com";
+ matrixService.setConfig(testConfig);
+ var url = "http://example.com/mypic.jpg";
+ matrixService.setProfilePictureUrl(url).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+ httpBackend.expectPUT(URL + "/profile/" +
+ encodeURIComponent(testConfig.user_id) +
+ "/avatar_url?access_token=foobar",
+ {
+ avatar_url: url
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to PUT /profile/$me/displayname', inject(
+ function(matrixService) {
+ var testConfig = angular.copy(CONFIG);
+ testConfig.user_id = "@bob:example.com";
+ matrixService.setConfig(testConfig);
+ var displayname = "Bob Smith";
+ matrixService.setDisplayName(displayname).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+ httpBackend.expectPUT(URL + "/profile/" +
+ encodeURIComponent(testConfig.user_id) +
+ "/displayname?access_token=foobar",
+ {
+ displayname: displayname
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to login with password', inject(
+ function(matrixService) {
+ matrixService.setConfig(CONFIG);
+ var userId = "@bob:example.com";
+ var password = "monkey";
+ matrixService.login(userId, password).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+ httpBackend.expectPOST(new RegExp(URL+"/login(.*)"),
+ {
+ user: userId,
+ password: password,
+ type: "m.login.password"
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+
+ it('should be able to PUT presence status', inject(
+ function(matrixService) {
+ var testConfig = angular.copy(CONFIG);
+ testConfig.user_id = "@bob:example.com";
+ matrixService.setConfig(testConfig);
+ var status = "unavailable";
+ matrixService.setUserPresence(status).then(function(response) {
+ expect(response.data).toEqual({});
+ });
+ httpBackend.expectPUT(URL+"/presence/"+
+ encodeURIComponent(testConfig.user_id)+
+ "/status?access_token=foobar",
+ {
+ presence: status
+ })
+ .respond({});
+ httpBackend.flush();
+ }));
+});
diff --git a/syweb/webclient/test/unit/model-service.spec.js b/syweb/webclient/test/unit/model-service.spec.js
new file mode 100644
index 0000000000..e2fa8ceba3
--- /dev/null
+++ b/syweb/webclient/test/unit/model-service.spec.js
@@ -0,0 +1,30 @@
+describe('ModelService', function() {
+
+ // setup the dependencies
+ beforeEach(function() {
+ // dependencies
+ module('matrixService');
+
+ // tested service
+ module('modelService');
+ });
+
+ it('should be able to get a member in a room', inject(
+ function(modelService) {
+ var roomId = "!wefiohwefuiow:matrix.org";
+ var userId = "@bob:matrix.org";
+
+ modelService.getRoom(roomId).current_room_state.storeStateEvent({
+ type: "m.room.member",
+ id: "fwefw:matrix.org",
+ user_id: userId,
+ state_key: userId,
+ content: {
+ membership: "join"
+ }
+ });
+
+ var user = modelService.getMember(roomId, userId);
+ expect(user.event.state_key).toEqual(userId);
+ }));
+});
diff --git a/syweb/webclient/test/unit/notification-service.spec.js b/syweb/webclient/test/unit/notification-service.spec.js
new file mode 100644
index 0000000000..4205ca0969
--- /dev/null
+++ b/syweb/webclient/test/unit/notification-service.spec.js
@@ -0,0 +1,78 @@
+describe('NotificationService', function() {
+
+ var userId = "@ali:matrix.org";
+ var displayName = "Alice M";
+ var bingWords = ["coffee","foo(.*)bar"]; // literal and wildcard
+
+ beforeEach(function() {
+ module('notificationService');
+ });
+
+ // User IDs
+
+ it('should bing on a user ID.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, "Hello @ali:matrix.org, how are you?")).toEqual(true);
+ }));
+
+ it('should bing on a partial user ID.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, "Hello @ali, how are you?")).toEqual(true);
+ }));
+
+ it('should bing on a case-insensitive user ID.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, "Hello @AlI:matrix.org, how are you?")).toEqual(true);
+ }));
+
+ // Display names
+
+ it('should bing on a display name.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, "Hello Alice M, how are you?")).toEqual(true);
+ }));
+
+ it('should bing on a case-insensitive display name.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, "Hello ALICE M, how are you?")).toEqual(true);
+ }));
+
+ // Bing words
+
+ it('should bing on a bing word.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, "I really like coffee")).toEqual(true);
+ }));
+
+ it('should bing on case-insensitive bing words.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, "Coffee is great")).toEqual(true);
+ }));
+
+ it('should bing on wildcard (.*) bing words.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, "It was foomahbar I think.")).toEqual(true);
+ }));
+
+ // invalid
+
+ it('should gracefully handle bad input.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, displayName,
+ bingWords, { "foo": "bar" })).toEqual(false);
+ }));
+
+ it('should gracefully handle just a user ID.', inject(
+ function(notificationService) {
+ expect(notificationService.containsBingWord(userId, undefined,
+ undefined, "Hello @ali:matrix.org, how are you?")).toEqual(true);
+ }));
+});
diff --git a/syweb/webclient/test/unit/recents-service.spec.js b/syweb/webclient/test/unit/recents-service.spec.js
new file mode 100644
index 0000000000..a2f9ecbaf8
--- /dev/null
+++ b/syweb/webclient/test/unit/recents-service.spec.js
@@ -0,0 +1,153 @@
+describe('RecentsService', function() {
+ var scope;
+ var MSG_EVENT = "__test__";
+
+ var testEventContainsBingWord, testIsLive, testEvent;
+
+ var eventHandlerService = {
+ MSG_EVENT: MSG_EVENT,
+ eventContainsBingWord: function(event) {
+ return testEventContainsBingWord;
+ }
+ };
+
+ // setup the service and mocked dependencies
+ beforeEach(function() {
+
+ // set default mock values
+ testEventContainsBingWord = false;
+ testIsLive = true;
+ testEvent = {
+ content: {
+ body: "Hello world",
+ msgtype: "m.text"
+ },
+ user_id: "@alfred:localhost",
+ room_id: "!fl1bb13:localhost",
+ event_id: "fwuegfw@localhost"
+ }
+
+ // mocked dependencies
+ module(function ($provide) {
+ $provide.value('eventHandlerService', eventHandlerService);
+ });
+
+ // tested service
+ module('recentsService');
+ });
+
+ beforeEach(inject(function($rootScope) {
+ scope = $rootScope;
+ }));
+
+ it('should start with no unread messages.', inject(
+ function(recentsService) {
+ expect(recentsService.getUnreadMessages()).toEqual({});
+ expect(recentsService.getUnreadBingMessages()).toEqual({});
+ }));
+
+ it('should NOT add an unread message to the room currently selected.', inject(
+ function(recentsService) {
+ recentsService.setSelectedRoomId(testEvent.room_id);
+ scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+ expect(recentsService.getUnreadMessages()).toEqual({});
+ expect(recentsService.getUnreadBingMessages()).toEqual({});
+ }));
+
+ it('should add an unread message to the room NOT currently selected.', inject(
+ function(recentsService) {
+ recentsService.setSelectedRoomId("!someotherroomid:localhost");
+ scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+
+ var unread = {};
+ unread[testEvent.room_id] = 1;
+ expect(recentsService.getUnreadMessages()).toEqual(unread);
+ }));
+
+ it('should add an unread message and an unread bing message if a message contains a bing word.', inject(
+ function(recentsService) {
+ recentsService.setSelectedRoomId("!someotherroomid:localhost");
+ testEventContainsBingWord = true;
+ scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+
+ var unread = {};
+ unread[testEvent.room_id] = 1;
+ expect(recentsService.getUnreadMessages()).toEqual(unread);
+
+ var bing = {};
+ bing[testEvent.room_id] = testEvent;
+ expect(recentsService.getUnreadBingMessages()).toEqual(bing);
+ }));
+
+ it('should clear both unread and unread bing messages when markAsRead is called.', inject(
+ function(recentsService) {
+ recentsService.setSelectedRoomId("!someotherroomid:localhost");
+ testEventContainsBingWord = true;
+ scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+
+ var unread = {};
+ unread[testEvent.room_id] = 1;
+ expect(recentsService.getUnreadMessages()).toEqual(unread);
+
+ var bing = {};
+ bing[testEvent.room_id] = testEvent;
+ expect(recentsService.getUnreadBingMessages()).toEqual(bing);
+
+ recentsService.markAsRead(testEvent.room_id);
+
+ unread[testEvent.room_id] = 0;
+ bing[testEvent.room_id] = undefined;
+ expect(recentsService.getUnreadMessages()).toEqual(unread);
+ expect(recentsService.getUnreadBingMessages()).toEqual(bing);
+ }));
+
+ it('should not add messages as unread if they are not live.', inject(
+ function(recentsService) {
+ testIsLive = false;
+
+ recentsService.setSelectedRoomId("!someotherroomid:localhost");
+ testEventContainsBingWord = true;
+ scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+
+ expect(recentsService.getUnreadMessages()).toEqual({});
+ expect(recentsService.getUnreadBingMessages()).toEqual({});
+ }));
+
+ it('should increment the unread message count.', inject(
+ function(recentsService) {
+ recentsService.setSelectedRoomId("!someotherroomid:localhost");
+ scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+
+ var unread = {};
+ unread[testEvent.room_id] = 1;
+ expect(recentsService.getUnreadMessages()).toEqual(unread);
+
+ scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+
+ unread[testEvent.room_id] = 2;
+ expect(recentsService.getUnreadMessages()).toEqual(unread);
+ }));
+
+ it('should set the bing event to the latest message to contain a bing word.', inject(
+ function(recentsService) {
+ recentsService.setSelectedRoomId("!someotherroomid:localhost");
+ testEventContainsBingWord = true;
+ scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+
+ var nextEvent = angular.copy(testEvent);
+ nextEvent.content.body = "Goodbye cruel world.";
+ nextEvent.event_id = "erfuerhfeaaaa@localhost";
+ scope.$broadcast(MSG_EVENT, nextEvent, testIsLive);
+
+ var bing = {};
+ bing[testEvent.room_id] = nextEvent;
+ expect(recentsService.getUnreadBingMessages()).toEqual(bing);
+ }));
+
+ it('should do nothing when marking an unknown room ID as read.', inject(
+ function(recentsService) {
+ recentsService.markAsRead("!someotherroomid:localhost");
+ expect(recentsService.getUnreadMessages()).toEqual({});
+ expect(recentsService.getUnreadBingMessages()).toEqual({});
+ }));
+});
diff --git a/syweb/webclient/test/unit/register-controller.spec.js b/syweb/webclient/test/unit/register-controller.spec.js
new file mode 100644
index 0000000000..b5c7842358
--- /dev/null
+++ b/syweb/webclient/test/unit/register-controller.spec.js
@@ -0,0 +1,84 @@
+describe("RegisterController ", function() {
+ var rootScope, scope, ctrl, $q, $timeout;
+ var userId = "@foo:bar";
+ var displayName = "Foo";
+ var avatarUrl = "avatar.url";
+
+ window.webClientConfig = {
+ useCapatcha: false
+ };
+
+ // test vars
+ var testRegisterData, testFailRegisterData;
+
+
+ // mock services
+ var matrixService = {
+ config: function() {
+ return {
+ user_id: userId
+ }
+ },
+ setConfig: function(){},
+ register: function(mxid, password, threepidCreds, useCaptcha) {
+ var d = $q.defer();
+ if (testFailRegisterData) {
+ d.reject({
+ data: testFailRegisterData
+ });
+ }
+ else {
+ d.resolve({
+ data: testRegisterData
+ });
+ }
+ return d.promise;
+ }
+ };
+
+ var eventStreamService = {};
+
+ beforeEach(function() {
+ module('matrixWebClient');
+
+ // reset test vars
+ testRegisterData = undefined;
+ testFailRegisterData = undefined;
+ });
+
+ beforeEach(inject(function($rootScope, $injector, $location, $controller, _$q_, _$timeout_) {
+ $q = _$q_;
+ $timeout = _$timeout_;
+ scope = $rootScope.$new();
+ rootScope = $rootScope;
+ routeParams = {
+ user_matrix_id: userId
+ };
+ ctrl = $controller('RegisterController', {
+ '$scope': scope,
+ '$rootScope': $rootScope,
+ '$location': $location,
+ 'matrixService': matrixService,
+ 'eventStreamService': eventStreamService
+ });
+ })
+ );
+
+ // SYWEB-109
+ it('should display an error if the HS rejects the username on registration', function() {
+ var prevFeedback = angular.copy(scope.feedback);
+
+ testFailRegisterData = {
+ errcode: "M_UNKNOWN",
+ error: "I am rejecting you."
+ };
+
+ scope.account.pwd1 = "password";
+ scope.account.pwd2 = "password";
+ scope.account.desired_user_id = "bob";
+ scope.register(); // this depends on the result of a deferred
+ rootScope.$digest(); // which is delivered after the digest
+
+ expect(scope.feedback).not.toEqual(prevFeedback);
+ });
+});
diff --git a/webclient/test/unit/user-controller.spec.js b/syweb/webclient/test/unit/user-controller.spec.js
index 798cc4de48..798cc4de48 100644
--- a/webclient/test/unit/user-controller.spec.js
+++ b/syweb/webclient/test/unit/user-controller.spec.js
diff --git a/webclient/user/user-controller.js b/syweb/webclient/user/user-controller.js
index 0dbfa325d0..0dbfa325d0 100644
--- a/webclient/user/user-controller.js
+++ b/syweb/webclient/user/user-controller.js
diff --git a/webclient/user/user.html b/syweb/webclient/user/user.html
index 2aa981437b..2aa981437b 100644
--- a/webclient/user/user.html
+++ b/syweb/webclient/user/user.html
|