Remove syweb directory. pull in syweb as a dependency from github
6
setup.py
@ -26,12 +26,13 @@ def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
setup(
|
||||
name="SynapseHomeServer",
|
||||
version="0.0.1",
|
||||
name="synapse",
|
||||
version=read("VERSION"),
|
||||
packages=find_packages(exclude=["tests", "tests.*"]),
|
||||
description="Reference Synapse Home Server",
|
||||
install_requires=[
|
||||
"syutil==0.0.2",
|
||||
"syweb==0.0.1",
|
||||
"Twisted>=14.0.0",
|
||||
"service_identity>=1.0.0",
|
||||
"pyopenssl>=0.14",
|
||||
@ -44,6 +45,7 @@ setup(
|
||||
dependency_links=[
|
||||
"https://github.com/matrix-org/syutil/tarball/v0.0.2#egg=syutil-0.0.2",
|
||||
"https://github.com/pyca/pynacl/tarball/52dbe2dc33f1#egg=pynacl-0.3.0",
|
||||
"https://github.com/matrix-org/matrix-angular-sdk/tarball/master/#egg=syweb-0.0.1",
|
||||
],
|
||||
setup_requires=[
|
||||
"setuptools_trial",
|
||||
|
@ -1,46 +0,0 @@
|
||||
Captcha can be enabled for this web client / home server. This file explains how to do that.
|
||||
The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google.
|
||||
|
||||
Getting keys
|
||||
------------
|
||||
Requires a public/private key pair from:
|
||||
|
||||
https://developers.google.com/recaptcha/
|
||||
|
||||
|
||||
Setting Private ReCaptcha Key
|
||||
-----------------------------
|
||||
The private key is a config option on the home server config. If it is not
|
||||
visible, you can generate it via --generate-config. Set the following value:
|
||||
|
||||
recaptcha_private_key: YOUR_PRIVATE_KEY
|
||||
|
||||
In addition, you MUST enable captchas via:
|
||||
|
||||
enable_registration_captcha: true
|
||||
|
||||
Setting Public ReCaptcha Key
|
||||
----------------------------
|
||||
The web client will look for the global variable webClientConfig for config
|
||||
options. You should put your ReCaptcha public key there like so:
|
||||
|
||||
webClientConfig = {
|
||||
useCaptcha: true,
|
||||
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
||||
}
|
||||
|
||||
This should be put in webclient/config.js which is already .gitignored, rather
|
||||
than in the web client source files. You MUST set useCaptcha to true else a
|
||||
ReCaptcha widget will not be generated.
|
||||
|
||||
Configuring IP used for auth
|
||||
----------------------------
|
||||
The ReCaptcha API requires that the IP address of the user who solved the
|
||||
captcha is sent. If the client is connecting through a proxy or load balancer,
|
||||
it may be required to use the X-Forwarded-For (XFF) header instead of the origin
|
||||
IP address. This can be configured as an option on the home server like so:
|
||||
|
||||
captcha_ip_origin_is_x_forwarded: true
|
||||
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
Basic Usage
|
||||
-----------
|
||||
|
||||
The web client should automatically run when running the home server.
|
||||
Alternatively, you can run it stand-alone:
|
||||
|
||||
$ python -m SimpleHTTPServer
|
||||
|
||||
Then, open this URL in a WEB browser::
|
||||
|
||||
http://127.0.0.1:8000/
|
||||
|
||||
|
@ -1,216 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Main controller
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
|
||||
.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();
|
||||
|
||||
// Update the location state when the ng location changed
|
||||
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
||||
$scope.location = $location.path();
|
||||
});
|
||||
|
||||
if (matrixService.isUserLoggedIn()) {
|
||||
eventStreamService.resume();
|
||||
mPresence.start();
|
||||
}
|
||||
|
||||
$scope.user_id;
|
||||
var config = matrixService.config();
|
||||
if (config) {
|
||||
$scope.user_id = matrixService.config().user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a given page.
|
||||
* @param {String} url url of the page
|
||||
*/
|
||||
$rootScope.goToPage = function(url) {
|
||||
$location.url(url);
|
||||
};
|
||||
|
||||
// Open the given user profile page
|
||||
$scope.goToUserPage = function(user_id) {
|
||||
if (user_id === $scope.user_id) {
|
||||
$location.url("/settings");
|
||||
}
|
||||
else {
|
||||
$location.url("/user/" + user_id);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.leave = function(room_id) {
|
||||
matrixService.leave(room_id).then(
|
||||
function(response) {
|
||||
console.log("Left room " + room_id);
|
||||
},
|
||||
function(error) {
|
||||
console.log("Failed to leave room " + room_id + ": " + error.data.error);
|
||||
});
|
||||
};
|
||||
|
||||
// Logs the user out
|
||||
$scope.logout = function() {
|
||||
|
||||
// kill the event stream
|
||||
eventStreamService.stop();
|
||||
|
||||
// Do not update presence anymore
|
||||
mPresence.stop();
|
||||
|
||||
// Clean permanent data
|
||||
matrixService.setConfig({});
|
||||
matrixService.saveConfig();
|
||||
|
||||
// Reset cached data
|
||||
modelService.clearRooms();
|
||||
eventHandlerService.reset();
|
||||
|
||||
// And go to the login page
|
||||
$location.url("login");
|
||||
};
|
||||
|
||||
// Listen to the event indicating that the access token is no longer valid.
|
||||
// In this case, the user needs to log in again.
|
||||
$scope.$on("M_UNKNOWN_TOKEN", function() {
|
||||
console.log("Invalid access token -> log user out");
|
||||
$scope.logout();
|
||||
});
|
||||
|
||||
$rootScope.updateHeader = function() {
|
||||
$scope.user_id = matrixService.config().user_id;
|
||||
};
|
||||
|
||||
$rootScope.$watch('currentCall', function(newVal, oldVal) {
|
||||
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
|
||||
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(modelService.getRoom($rootScope.currentCall.room_id).current_room_state.members);
|
||||
delete roomMembers[matrixService.config().user_id];
|
||||
|
||||
$rootScope.currentCall.user_id = Object.keys(roomMembers)[0];
|
||||
|
||||
// set it to the user ID until we fetch the display name
|
||||
$rootScope.currentCall.userProfile = { displayname: $rootScope.currentCall.user_id };
|
||||
|
||||
matrixService.getProfile($rootScope.currentCall.user_id).then(
|
||||
function(response) {
|
||||
if (response.data.displayname) $rootScope.currentCall.userProfile.displayname = response.data.displayname;
|
||||
if (response.data.avatar_url) $rootScope.currentCall.userProfile.avatar_url = response.data.avatar_url;
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't load user profile";
|
||||
}
|
||||
);
|
||||
});
|
||||
$rootScope.$watch('currentCall.state', function(newVal, oldVal) {
|
||||
if (newVal == 'ringing') {
|
||||
angular.element('#ringbackAudio')[0].pause();
|
||||
angular.element('#ringAudio')[0].load();
|
||||
angular.element('#ringAudio')[0].play();
|
||||
} else if (newVal == 'invite_sent') {
|
||||
angular.element('#ringAudio')[0].pause();
|
||||
angular.element('#ringbackAudio')[0].load();
|
||||
angular.element('#ringbackAudio')[0].play();
|
||||
} else if (newVal == 'ended' && oldVal == 'connected') {
|
||||
angular.element('#ringAudio')[0].pause();
|
||||
angular.element('#ringbackAudio')[0].pause();
|
||||
angular.element('#callendAudio')[0].play();
|
||||
$scope.videoMode = undefined;
|
||||
} else if (newVal == 'ended' && oldVal == 'invite_sent' && $rootScope.currentCall.hangupParty == 'remote') {
|
||||
angular.element('#ringAudio')[0].pause();
|
||||
angular.element('#ringbackAudio')[0].pause();
|
||||
angular.element('#busyAudio')[0].play();
|
||||
} else if (newVal == 'ended' && oldVal == 'invite_sent' && $rootScope.currentCall.hangupParty == 'local' && $rootScope.currentCall.hangupReason == 'invite_timeout') {
|
||||
angular.element('#ringAudio')[0].pause();
|
||||
angular.element('#ringbackAudio')[0].pause();
|
||||
angular.element('#busyAudio')[0].play();
|
||||
} else if (oldVal == 'invite_sent') {
|
||||
angular.element('#ringbackAudio')[0].pause();
|
||||
} else if (oldVal == 'ringing') {
|
||||
angular.element('#ringAudio')[0].pause();
|
||||
} else if (newVal == 'connected') {
|
||||
$timeout(function() {
|
||||
if ($scope.currentCall.type == 'video') $scope.videoMode = 'large';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if ($rootScope.currentCall && $rootScope.currentCall.type == 'video' && $rootScope.currentCall.state != 'connected') {
|
||||
$scope.videoMode = 'mini';
|
||||
}
|
||||
});
|
||||
$rootScope.$watch('currentCall.type', function(newVal, oldVal) {
|
||||
// need to listen for this too as the type of the call won't be know when it's created
|
||||
if ($rootScope.currentCall && $rootScope.currentCall.type == 'video' && $rootScope.currentCall.state != 'connected') {
|
||||
$scope.videoMode = 'mini';
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
|
||||
console.log("incoming call");
|
||||
if ($rootScope.currentCall && $rootScope.currentCall.state != 'ended') {
|
||||
console.log("rejecting call because we're already in a call");
|
||||
call.hangup();
|
||||
return;
|
||||
}
|
||||
call.onError = $scope.onCallError;
|
||||
call.onHangup = $scope.onCallHangup;
|
||||
call.localVideoSelector = '#localVideo';
|
||||
call.remoteVideoSelector = '#remoteVideo';
|
||||
$rootScope.currentCall = call;
|
||||
});
|
||||
|
||||
$rootScope.$on(matrixPhoneService.REPLACED_CALL_EVENT, function(ngEvent, oldCall, newCall) {
|
||||
console.log("call ID "+oldCall.call_id+" has been replaced by call ID "+newCall.call_id+"!");
|
||||
newCall.onError = $scope.onCallError;
|
||||
newCall.onHangup = $scope.onCallHangup;
|
||||
$rootScope.currentCall = newCall;
|
||||
});
|
||||
|
||||
$scope.answerCall = function() {
|
||||
$rootScope.currentCall.answer();
|
||||
};
|
||||
|
||||
$scope.hangupCall = function() {
|
||||
$rootScope.currentCall.hangup();
|
||||
};
|
||||
|
||||
$rootScope.onCallError = function(errStr) {
|
||||
$scope.feedback = errStr;
|
||||
};
|
||||
|
||||
$rootScope.onCallHangup = function(call) {
|
||||
if (call == $rootScope.currentCall) {
|
||||
$timeout(function(){
|
||||
if (call == $rootScope.currentCall) $rootScope.currentCall = undefined;
|
||||
}, 4070);
|
||||
}
|
||||
};
|
||||
}]);
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
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('matrixWebClient')
|
||||
.directive('ngEnter', function () {
|
||||
return function (scope, element, attrs) {
|
||||
element.bind("keydown keypress", function (event) {
|
||||
if(event.which === 13) {
|
||||
scope.$apply(function () {
|
||||
scope.$eval(attrs.ngEnter);
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
.directive('ngFocus', ['$timeout', function($timeout) {
|
||||
return {
|
||||
link: function(scope, element, attr) {
|
||||
// XXX: slightly evil hack to disable autofocus on iOS, as in general
|
||||
// it causes more problems than it fixes, by bouncing the page
|
||||
// around
|
||||
if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) {
|
||||
$timeout(function() { element[0].focus(); }, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
}])
|
||||
.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);
|
||||
}
|
||||
}
|
||||
});
|
@ -1,138 +0,0 @@
|
||||
/*
|
||||
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('matrixWebClient')
|
||||
.filter('duration', function() {
|
||||
return function(time) {
|
||||
if (!time) return;
|
||||
var t = parseInt(time / 1000);
|
||||
var s = t % 60;
|
||||
var m = parseInt(t / 60) % 60;
|
||||
var h = parseInt(t / (60 * 60)) % 24;
|
||||
var d = parseInt(t / (60 * 60 * 24));
|
||||
if (t < 60) {
|
||||
if (t < 0) {
|
||||
return "0s";
|
||||
}
|
||||
return s + "s";
|
||||
}
|
||||
if (t < 60 * 60) {
|
||||
return m + "m"; // + s + "s";
|
||||
}
|
||||
if (t < 24 * 60 * 60) {
|
||||
return h + "h"; // + m + "m";
|
||||
}
|
||||
return d + "d "; // + h + "h";
|
||||
};
|
||||
})
|
||||
.filter('orderMembersList', function($sce) {
|
||||
return function(members) {
|
||||
var filtered = [];
|
||||
|
||||
var displayNames = {};
|
||||
angular.forEach(members, function(value, key) {
|
||||
value["id"] = key;
|
||||
filtered.push( value );
|
||||
});
|
||||
|
||||
filtered.sort(function (a, b) {
|
||||
// Sort members on their last_active absolute time
|
||||
a = a.user;
|
||||
b = b.user;
|
||||
|
||||
var aLastActiveTS = 0, bLastActiveTS = 0;
|
||||
if (a && a.event && a.event.content && a.event.content.last_active_ago !== undefined) {
|
||||
aLastActiveTS = a.last_updated - a.event.content.last_active_ago;
|
||||
}
|
||||
if (b && b.event && b.event.content && b.event.content.last_active_ago !== undefined) {
|
||||
bLastActiveTS = b.last_updated - b.event.content.last_active_ago;
|
||||
}
|
||||
if (aLastActiveTS || bLastActiveTS) {
|
||||
return bLastActiveTS - aLastActiveTS;
|
||||
}
|
||||
else {
|
||||
// If they do not have last_active_ago, sort them according to their presence state
|
||||
// Online users go first amongs members who do not have last_active_ago
|
||||
var presenceLevels = {
|
||||
offline: 1,
|
||||
unavailable: 2,
|
||||
online: 4,
|
||||
free_for_chat: 3
|
||||
};
|
||||
var aPresence = (a && a.event && a.event.content.presence in presenceLevels) ? presenceLevels[a.event.content.presence] : 0;
|
||||
var bPresence = (b && b.event && b.event.content.presence in presenceLevels) ? presenceLevels[b.event.content.presence] : 0;
|
||||
return bPresence - aPresence;
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
};
|
||||
})
|
||||
.filter('unsafe', ['$sce', function($sce) {
|
||||
return function(text) {
|
||||
return $sce.trustAsHtml(text);
|
||||
};
|
||||
}])
|
||||
// Exactly the same as ngSanitize's linky but instead of pushing sanitized
|
||||
// text in the addText function, we just push the raw text.
|
||||
.filter('unsanitizedLinky', ['$sanitize', function($sanitize) {
|
||||
var LINKY_URL_REGEXP =
|
||||
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
|
||||
MAILTO_REGEXP = /^mailto:/;
|
||||
|
||||
return function(text, target) {
|
||||
if (!text) return text;
|
||||
var match;
|
||||
var raw = text;
|
||||
var html = [];
|
||||
var url;
|
||||
var i;
|
||||
while ((match = raw.match(LINKY_URL_REGEXP))) {
|
||||
// We can not end in these as they are sometimes found at the end of the sentence
|
||||
url = match[0];
|
||||
// if we did not match ftp/http/mailto then assume mailto
|
||||
if (match[2] == match[3]) url = 'mailto:' + url;
|
||||
i = match.index;
|
||||
addText(raw.substr(0, i));
|
||||
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
|
||||
raw = raw.substring(i + match[0].length);
|
||||
}
|
||||
addText(raw);
|
||||
return $sanitize(html.join(''));
|
||||
|
||||
function addText(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
html.push(text);
|
||||
}
|
||||
|
||||
function addLink(url, text) {
|
||||
html.push('<a ');
|
||||
if (angular.isDefined(target)) {
|
||||
html.push('target="');
|
||||
html.push(target);
|
||||
html.push('" ');
|
||||
}
|
||||
html.push('href="');
|
||||
html.push(url);
|
||||
html.push('">');
|
||||
addText(text);
|
||||
html.push('</a>');
|
||||
}
|
||||
};
|
||||
}]);
|
@ -1,893 +0,0 @@
|
||||
/** Common layout **/
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
||||
font-size: 12pt;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20pt;
|
||||
}
|
||||
|
||||
a:link { color: #666; }
|
||||
a:visited { color: #666; }
|
||||
a:hover { color: #000; }
|
||||
a:active { color: #000; }
|
||||
|
||||
textarea, input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100%;
|
||||
margin-bottom: -32px; /* to make room for the footer */
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
margin: auto;
|
||||
max-width: 1280px;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
#unsupportedBrowser {
|
||||
padding-top: 240px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#header
|
||||
{
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
background-color: #333;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
#callBar {
|
||||
float: left;
|
||||
height: 32px;
|
||||
margin: auto;
|
||||
text-align: right;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.callIcon {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
margin-top: 8px;
|
||||
transition: transform linear 0.5s;
|
||||
transition: -webkit-transform linear 0.5s;
|
||||
}
|
||||
|
||||
.callIcon.ended {
|
||||
transform: rotateZ(45deg);
|
||||
-webkit-transform: rotateZ(45deg);
|
||||
filter: hue-rotate(-90deg);
|
||||
-webkit-filter: hue-rotate(-90deg);
|
||||
}
|
||||
|
||||
#callPeerImage {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#callPeerNameAndState {
|
||||
float: left;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
#callState {
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
#callPeerName {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
#videoBackground {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 1;
|
||||
background-color: rgba(0,0,0,0.0);
|
||||
pointer-events: none;
|
||||
transition: background-color linear 500ms;
|
||||
}
|
||||
|
||||
#videoBackground.large {
|
||||
background-color: rgba(0,0,0,0.85);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#videoContainer {
|
||||
position: relative;
|
||||
top: 32px;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#videoContainerPadding {
|
||||
width: 1280px;
|
||||
}
|
||||
|
||||
#localVideo {
|
||||
position: absolute;
|
||||
width: 128px;
|
||||
height: 72px;
|
||||
z-index: 1;
|
||||
transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms;
|
||||
}
|
||||
|
||||
.mini #localVideo {
|
||||
top: 0px;
|
||||
left: 130px;
|
||||
}
|
||||
|
||||
.large #localVideo {
|
||||
top: 70px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.ended #localVideo {
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
#remoteVideo {
|
||||
position: relative;
|
||||
height: auto;
|
||||
transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms;
|
||||
}
|
||||
|
||||
.mini #remoteVideo {
|
||||
left: 260px;
|
||||
top: 0px;
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
.large #remoteVideo {
|
||||
left: 0px;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ended #remoteVideo {
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
#headerContent {
|
||||
color: #ccc;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
text-align: right;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#headerContent a:link,
|
||||
#headerContent a:visited,
|
||||
#headerContent a:hover,
|
||||
#headerContent a:active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#footer
|
||||
{
|
||||
width: 100%;
|
||||
border-top: #666 1px solid;
|
||||
background-color: #aaa;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
#footerContent
|
||||
{
|
||||
font-size: 8pt;
|
||||
color: #fff;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
#genericHeading
|
||||
{
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
#feedback {
|
||||
color: #800;
|
||||
}
|
||||
|
||||
.mouse-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.invited {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/*** Login Pages ***/
|
||||
|
||||
.loginWrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#recaptcha_area {
|
||||
margin: auto
|
||||
}
|
||||
|
||||
#loginForm {
|
||||
text-align: left;
|
||||
padding: 1em;
|
||||
margin-bottom: 40px;
|
||||
display: inline-block;
|
||||
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||
|
||||
background-color: #f8f8f8;
|
||||
border: 1px #ccc solid;
|
||||
}
|
||||
|
||||
#loginForm input[type='radio'] {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#serverConfig {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#serverConfig,
|
||||
#serverConfig input,
|
||||
#serverConfig button
|
||||
{
|
||||
font-size: 10pt ! important;
|
||||
}
|
||||
|
||||
.smallPrint {
|
||||
color: #888;
|
||||
font-size: 9pt ! important;
|
||||
font-style: italic ! important;
|
||||
}
|
||||
|
||||
#serverConfig label {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
margin-right: 0.5em;
|
||||
width: 7em;
|
||||
}
|
||||
|
||||
#loginForm,
|
||||
#loginForm input,
|
||||
#loginForm button,
|
||||
#loginForm select {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/*** Room page ***/
|
||||
|
||||
#roomPage {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
bottom: 120px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
#roomWrapper {
|
||||
margin: auto;
|
||||
max-width: 1280px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#roomHeader {
|
||||
margin: auto;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 53px;
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
background-color: #f8f8f8;
|
||||
border-top: #aaa 1px solid;
|
||||
}
|
||||
|
||||
#controls {
|
||||
max-width: 1280px;
|
||||
padding: 12px;
|
||||
padding-right: 42px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#buttonsCell {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#inputBarTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#inputBarTable tr td {
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
#mainInput {
|
||||
width: 100%;
|
||||
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 {
|
||||
background-color: #faa;
|
||||
}
|
||||
|
||||
.roomHighlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.publicTable {
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.publicTable tr {
|
||||
width: 100%;
|
||||
}
|
||||
.publicTable td {
|
||||
vertical-align: text-top;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.publicRoomEntry {
|
||||
max-width: 430px;
|
||||
}
|
||||
|
||||
.publicRoomJoinedUsers {
|
||||
width: 5em;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.publicRoomTopic {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px #ddd solid;
|
||||
}
|
||||
|
||||
#roomName {
|
||||
font-size: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#roomTopic {
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.roomNameInput, .roomTopicInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roomNameSection, .roomTopicSection {
|
||||
text-align: right;
|
||||
float: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roomNameSetNew, .roomTopicSetNew {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.roomHeaderInfo {
|
||||
text-align: right;
|
||||
float: right;
|
||||
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;
|
||||
clear: right;
|
||||
width: 101px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
#usersTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
#usersTable td {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.userAvatar {
|
||||
width: 80px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
background-color: #000;
|
||||
}
|
||||
*/
|
||||
|
||||
.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;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
.userAvatar .userAvatarGradient {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
*/
|
||||
|
||||
.userName {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 6px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.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;
|
||||
background-color: #f00;
|
||||
}
|
||||
|
||||
/*
|
||||
.userPresence {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: #aaa;
|
||||
border-bottom: 1px #ddd solid;
|
||||
}
|
||||
*/
|
||||
|
||||
.online {
|
||||
border-color: #38AF00;
|
||||
background-color: #38AF00;
|
||||
}
|
||||
|
||||
.unavailable {
|
||||
border-color: #FFCC00;
|
||||
background-color: #FFCC00;
|
||||
}
|
||||
|
||||
/*** Message table ***/
|
||||
|
||||
#messageTableWrapper {
|
||||
height: 100%;
|
||||
margin-right: 140px;
|
||||
overflow-y: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#messageTable {
|
||||
margin: auto;
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
#messageTable td {
|
||||
padding: 0px;
|
||||
/* border: 1px solid #888; */
|
||||
}
|
||||
|
||||
.leftBlock {
|
||||
width: 7em;
|
||||
word-wrap: break-word;
|
||||
vertical-align: top;
|
||||
background-color: #fff;
|
||||
color: #aaa;
|
||||
font-weight: medium;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
/*
|
||||
border-top: 1px #ddd solid;
|
||||
*/
|
||||
}
|
||||
|
||||
.rightBlock {
|
||||
width: 32px;
|
||||
color: #888;
|
||||
line-height: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.sender, .timestamp {
|
||||
/* padding-top: 3px;
|
||||
*/}
|
||||
|
||||
.timestamp {
|
||||
font-size: 10px;
|
||||
color: #ccc;
|
||||
height: 13px;
|
||||
margin-top: 4px;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.3s;
|
||||
}
|
||||
|
||||
.sender {
|
||||
font-size: 12px;
|
||||
/*
|
||||
margin-top: 5px;
|
||||
margin-bottom: -9px;
|
||||
*/
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.avatarImage {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
object-fit: cover;
|
||||
border-radius: 32px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.emote {
|
||||
background-color: transparent ! important;
|
||||
border: 0px ! important;
|
||||
}
|
||||
|
||||
.membership {
|
||||
background-color: transparent ! important;
|
||||
border: 0px ! important;
|
||||
}
|
||||
|
||||
.image {
|
||||
border: 1px solid #888;
|
||||
display: block;
|
||||
max-width:320px;
|
||||
max-height:320px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.text {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
/*
|
||||
background-color: #eee;
|
||||
border: 1px solid #d8d8d8;
|
||||
margin-bottom: -1px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 5px;
|
||||
-webkit-text-size-adjust:100%
|
||||
vertical-align: middle;
|
||||
*/
|
||||
display: inline-block;
|
||||
max-width: 80%;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-size: 14px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.bubble img {
|
||||
max-width: 100%;
|
||||
max-height: auto;
|
||||
}
|
||||
|
||||
.differentUser .msg {
|
||||
padding-top: 14px ! important;
|
||||
}
|
||||
|
||||
.mine {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/*
|
||||
.text.emote .bubble,
|
||||
.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;
|
||||
}
|
||||
|
||||
.bubble .message {
|
||||
/* Wrap words and break lines on CR+LF */
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.bubble .messagePending {
|
||||
opacity: 0.3
|
||||
}
|
||||
.messageUnSent {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
.messageBing {
|
||||
color: #00F;
|
||||
}
|
||||
|
||||
#room-fullscreen-image {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#room-fullscreen-image img {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
|
||||
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
|
||||
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
|
||||
}
|
||||
|
||||
/*** Recents ***/
|
||||
|
||||
.recentsTable {
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.recentsTable tr {
|
||||
width: 100%;
|
||||
}
|
||||
.recentsTable td {
|
||||
vertical-align: text-top;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.recentsRoom {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.recentsRoom:hover {
|
||||
background-color: #f8f8ff;
|
||||
}
|
||||
|
||||
.recentsRoomSelected {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.recentsRoomUnread {
|
||||
background-color: #fee;
|
||||
}
|
||||
|
||||
.recentsRoomBing {
|
||||
background-color: #eef;
|
||||
}
|
||||
|
||||
.recentsRoomName {
|
||||
font-size: 16px;
|
||||
padding-top: 7px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.recentsPublicRoom {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.recentsRoomSummaryUsersCount, .recentsRoomSummaryTS {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
width: 7em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.recentsRoomSummary {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Do not show users count in the recents fragment displayed on the room page */
|
||||
#roomPage .recentsRoomSummaryUsersCount {
|
||||
width: 0em;
|
||||
}
|
||||
|
||||
/*** Recents in the room page ***/
|
||||
|
||||
#roomRecentsTableWrapper {
|
||||
float: left;
|
||||
max-width: 320px;
|
||||
padding-right: 10px;
|
||||
margin-right: 10px;
|
||||
height: 100%;
|
||||
/* border-right: 1px solid #ddd; */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/*** Profile ***/
|
||||
|
||||
.profile-avatar {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/*** User profile page ***/
|
||||
|
||||
#user-displayname {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#user-displayname-input {
|
||||
width: 160px;
|
||||
max-width: 155px;
|
||||
}
|
||||
|
||||
#user-save-button {
|
||||
width: 160px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
var matrixWebClient = angular.module('matrixWebClient', [
|
||||
'ngRoute',
|
||||
'MatrixWebClientController',
|
||||
'LoginController',
|
||||
'RegisterController',
|
||||
'RoomController',
|
||||
'HomeController',
|
||||
'RecentsController',
|
||||
'SettingsController',
|
||||
'UserController',
|
||||
'matrixService',
|
||||
'matrixPhoneService',
|
||||
'MatrixCall',
|
||||
'eventStreamService',
|
||||
'eventHandlerService',
|
||||
'notificationService',
|
||||
'recentsService',
|
||||
'modelService',
|
||||
'commandsService',
|
||||
'infinite-scroll',
|
||||
'ui.bootstrap',
|
||||
'monospaced.elastic'
|
||||
]);
|
||||
|
||||
matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
|
||||
function($routeProvider, $provide, $httpProvider) {
|
||||
$routeProvider.
|
||||
when('/login', {
|
||||
templateUrl: 'login/login.html'
|
||||
}).
|
||||
when('/register', {
|
||||
templateUrl: 'login/register.html'
|
||||
}).
|
||||
when('/room/:room_id_or_alias', {
|
||||
templateUrl: 'room/room.html'
|
||||
}).
|
||||
when('/room/', { // room URL with room alias in it (ex: http://127.0.0.1:8000/#/room/#public:localhost:8080) will come here.
|
||||
// The reason is that 2nd hash key breaks routeProvider parameters cutting so that the URL will not match with
|
||||
// the previous '/room/:room_id_or_alias' URL rule
|
||||
templateUrl: 'room/room.html'
|
||||
}).
|
||||
when('/', {
|
||||
templateUrl: 'home/home.html'
|
||||
}).
|
||||
when('/settings', {
|
||||
templateUrl: 'settings/settings.html'
|
||||
}).
|
||||
when('/user/:user_matrix_id', {
|
||||
templateUrl: 'user/user.html'
|
||||
}).
|
||||
otherwise({
|
||||
redirectTo: '/'
|
||||
});
|
||||
|
||||
$provide.factory('AccessTokenInterceptor', ['$q', '$rootScope',
|
||||
function ($q, $rootScope) {
|
||||
return {
|
||||
responseError: function(rejection) {
|
||||
if (rejection.status === 403 && "data" in rejection &&
|
||||
"errcode" in rejection.data &&
|
||||
rejection.data.errcode === "M_UNKNOWN_TOKEN") {
|
||||
console.log("Got a 403 with an unknown token. Logging out.")
|
||||
$rootScope.$broadcast("M_UNKNOWN_TOKEN");
|
||||
}
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
$httpProvider.interceptors.push('AccessTokenInterceptor');
|
||||
}]);
|
||||
|
||||
matrixWebClient.run(['$location', '$rootScope', 'matrixService', function($location, $rootScope, matrixService) {
|
||||
|
||||
// Check browser support
|
||||
// Support IE from 9.0. AngularJS needs some tricks to run on IE8 and below
|
||||
var version = parseFloat($.browser.version);
|
||||
if ($.browser.msie && version < 9.0) {
|
||||
$rootScope.unsupportedBrowser = {
|
||||
browser: navigator.userAgent,
|
||||
reason: "Internet Explorer is supported from version 9"
|
||||
};
|
||||
}
|
||||
// The app requires localStorage
|
||||
if(typeof(Storage) === "undefined") {
|
||||
$rootScope.unsupportedBrowser = {
|
||||
browser: navigator.userAgent,
|
||||
reason: "It does not support HTML local storage"
|
||||
};
|
||||
}
|
||||
|
||||
// If user auth details are not in cache, go to the login page
|
||||
if (!matrixService.isUserLoggedIn() &&
|
||||
$location.path() !== "/login" &&
|
||||
$location.path() !== "/register")
|
||||
{
|
||||
$location.path("login");
|
||||
}
|
||||
|
||||
}]);
|
5081
syweb/webclient/bootstrap.css
vendored
@ -1,57 +0,0 @@
|
||||
/*
|
||||
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';
|
||||
|
||||
/*
|
||||
* Transform an element into an image file input button.
|
||||
* Watch to the passed variable change. It will contain the selected HTML5 file object.
|
||||
*/
|
||||
angular.module('mFileInput', [])
|
||||
.directive('mFileInput', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
transclude: 'true',
|
||||
// FIXME: add back in accept="image/*" when needed - e.g. for avatars
|
||||
template: '<div ng-transclude></div><input ng-hide="true" type="file"/>',
|
||||
scope: {
|
||||
selectedFile: '=mFileInput'
|
||||
},
|
||||
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
|
||||
// Check if HTML5 file selection is supported
|
||||
if (window.FileList) {
|
||||
element.bind("click", function() {
|
||||
element.find("input")[0].click();
|
||||
element.find("input").bind("change", function(e) {
|
||||
scope.selectedFile = this.files[0];
|
||||
scope.$apply();
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
setTimeout(function() {
|
||||
element.attr("disabled", true);
|
||||
element.attr("title", "The app uses the HTML5 File API to send files. Your browser does not support it.");
|
||||
}, 1);
|
||||
}
|
||||
|
||||
// Change the mouse icon on mouseover on this element
|
||||
element.css("cursor", "pointer");
|
||||
}
|
||||
};
|
||||
});
|
@ -1,208 +0,0 @@
|
||||
/*
|
||||
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';
|
||||
|
||||
// TODO determine if this is really required as a separate service to matrixService.
|
||||
/*
|
||||
* Upload an HTML5 file to a server
|
||||
*/
|
||||
angular.module('mFileUpload', ['matrixService', 'mUtilities'])
|
||||
.service('mFileUpload', ['$q', 'matrixService', 'mUtilities', function ($q, matrixService, mUtilities) {
|
||||
|
||||
/*
|
||||
* Upload an HTML5 file or blob to a server and returned a promise
|
||||
* that will provide the URL of the uploaded file.
|
||||
* @param {File|Blob} file the file data to send
|
||||
*/
|
||||
this.uploadFile = function(file) {
|
||||
var deferred = $q.defer();
|
||||
console.log("Uploading " + file.name + "... to /_matrix/content");
|
||||
matrixService.uploadContent(file).then(
|
||||
function(response) {
|
||||
var content_url = response.data.content_token;
|
||||
console.log(" -> Successfully uploaded! Available at " + content_url);
|
||||
deferred.resolve(content_url);
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Failed to upload " + file.name);
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/*
|
||||
* Upload an file plus generate a thumbnail of it (if possible) and upload it so that
|
||||
* we will have all information to fulfill an file/image message request
|
||||
* @param {File} file the file to send
|
||||
* @param {Integer} thumbnailSize the max side size of the thumbnail to create
|
||||
* @returns {promise} A promise that will be resolved by a message object
|
||||
* ready to be send with the Matrix API
|
||||
*/
|
||||
this.uploadFileAndThumbnail = function(file, thumbnailSize) {
|
||||
var self = this;
|
||||
var deferred = $q.defer();
|
||||
|
||||
console.log("uploadFileAndThumbnail " + file.name + " - thumbnailSize: " + thumbnailSize);
|
||||
|
||||
// The message structure that will be returned in the promise will look something like:
|
||||
var message = {
|
||||
/*
|
||||
msgtype: "m.image",
|
||||
url: undefined,
|
||||
body: "Image",
|
||||
info: {
|
||||
size: undefined,
|
||||
w: undefined,
|
||||
h: undefined,
|
||||
mimetype: undefined
|
||||
},
|
||||
thumbnail_url: undefined,
|
||||
thumbnail_info: {
|
||||
size: undefined,
|
||||
w: undefined,
|
||||
h: undefined,
|
||||
mimetype: undefined
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
if (file.type.indexOf("image/") === 0) {
|
||||
// it's an image - try to do clientside thumbnailing.
|
||||
mUtilities.getImageSize(file).then(
|
||||
function(size) {
|
||||
console.log("image size: " + JSON.stringify(size));
|
||||
|
||||
// The final operation: send file
|
||||
var uploadImage = function() {
|
||||
self.uploadFile(file).then(
|
||||
function(url) {
|
||||
// Update message metadata
|
||||
message.url = url;
|
||||
message.msgtype = "m.image";
|
||||
message.body = file.name;
|
||||
message.info = {
|
||||
size: file.size,
|
||||
w: size.width,
|
||||
h: size.height,
|
||||
mimetype: file.type
|
||||
};
|
||||
|
||||
// If there is no thumbnail (because the original image is smaller than thumbnailSize),
|
||||
// reuse the original image info for thumbnail data
|
||||
if (!message.thumbnail_url) {
|
||||
message.thumbnail_url = message.url;
|
||||
message.thumbnail_info = message.info;
|
||||
}
|
||||
|
||||
// We are done
|
||||
deferred.resolve(message);
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Can't upload image");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Create a thumbnail if the image size exceeds thumbnailSize
|
||||
if (Math.max(size.width, size.height) > thumbnailSize) {
|
||||
console.log(" Creating thumbnail...");
|
||||
mUtilities.resizeImage(file, thumbnailSize).then(
|
||||
function(thumbnailBlob) {
|
||||
|
||||
// Get its size
|
||||
mUtilities.getImageSize(thumbnailBlob).then(
|
||||
function(thumbnailSize) {
|
||||
console.log(" -> Thumbnail size: " + JSON.stringify(thumbnailSize));
|
||||
|
||||
// Upload it to the server
|
||||
self.uploadFile(thumbnailBlob).then(
|
||||
function(thumbnailUrl) {
|
||||
|
||||
// Update image message data
|
||||
message.thumbnail_url = thumbnailUrl;
|
||||
message.thumbnail_info = {
|
||||
size: thumbnailBlob.size,
|
||||
w: thumbnailSize.width,
|
||||
h: thumbnailSize.height,
|
||||
mimetype: thumbnailBlob.type
|
||||
};
|
||||
|
||||
// Then, upload the original image
|
||||
uploadImage();
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Can't upload thumbnail");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Failed to get thumbnail size");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Failed to create thumbnail: " + error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
// No need of thumbnail
|
||||
console.log(" Thumbnail is not required");
|
||||
uploadImage();
|
||||
}
|
||||
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Failed to get image size");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
// it's a random file - just upload it.
|
||||
self.uploadFile(file).then(
|
||||
function(url) {
|
||||
// Update message metadata
|
||||
message.url = url;
|
||||
message.msgtype = "m.file";
|
||||
message.body = file.name;
|
||||
message.info = {
|
||||
size: file.size,
|
||||
mimetype: file.type
|
||||
};
|
||||
|
||||
// We are done
|
||||
deferred.resolve(message);
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Can't upload file");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
}]);
|
@ -1,164 +0,0 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
|
@ -1,557 +0,0 @@
|
||||
/*
|
||||
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();
|
||||
|
||||
// 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);
|
||||
if (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.");
|
||||
}
|
||||
};
|
||||
|
||||
// resolves a room ID or alias, returning a deferred.
|
||||
var resolveRoomIdentifier = function(roomIdOrAlias) {
|
||||
var defer = $q.defer();
|
||||
if ('#' === roomIdOrAlias[0]) {
|
||||
matrixService.resolveRoomAlias(roomIdOrAlias).then(function(response) {
|
||||
defer.resolve(response.data.room_id);
|
||||
console.log("resolveRoomIdentifier: "+roomIdOrAlias+" -> " + response.data.room_id);
|
||||
},
|
||||
function(err) {
|
||||
console.error("resolveRoomIdentifier: lookup failed. "+JSON.stringify(err.data));
|
||||
defer.reject(err.data);
|
||||
});
|
||||
}
|
||||
else if ('!' === roomIdOrAlias[0]) {
|
||||
defer.resolve(roomIdOrAlias);
|
||||
}
|
||||
else {
|
||||
console.error("resolveRoomIdentifier: Unknown roomIdOrAlias => "+roomIdOrAlias);
|
||||
defer.reject("Bad room identifier: "+roomIdOrAlias);
|
||||
}
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
// joins a room and handles the requests which need to be done e.g. getting room state
|
||||
joinRoom: function(roomIdOrAlias) {
|
||||
var defer = $q.defer();
|
||||
var eventHandlerService = this;
|
||||
|
||||
var errorFunc = function(error) {
|
||||
console.error("joinRoom: " + JSON.stringify(error));
|
||||
defer.reject(error);
|
||||
};
|
||||
|
||||
resolveRoomIdentifier(roomIdOrAlias).then(function(roomId) {
|
||||
// check if you are joined already
|
||||
eventHandlerService.waitForInitialSyncCompletion().then(function() {
|
||||
var members = modelService.getRoom(roomId).current_room_state.members;
|
||||
var me = matrixService.config().user_id;
|
||||
if (me in members) {
|
||||
if ("join" === members[me].event.content.membership) {
|
||||
console.log("joinRoom: Already joined room "+roomId);
|
||||
defer.resolve(roomId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// join the room and get current room state
|
||||
matrixService.join(roomId).then(function() {
|
||||
matrixService.roomState(roomId).then(function(response) {
|
||||
var room = modelService.getRoom(roomId);
|
||||
room.current_room_state.storeStateEvents(response.data);
|
||||
room.old_room_state.storeStateEvents(response.data);
|
||||
console.log("joinRoom: Joined room "+roomId);
|
||||
defer.resolve(roomId);
|
||||
}, errorFunc);
|
||||
}, errorFunc);
|
||||
}, errorFunc);
|
||||
}, errorFunc);
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
// Returns a promise that resolves when the initialSync request has been processed
|
||||
waitForInitialSyncCompletion: function() {
|
||||
return initialSyncDeferred.promise;
|
||||
},
|
||||
|
||||
eventContainsBingWord: function(event) {
|
||||
return containsBingWord(event);
|
||||
}
|
||||
|
||||
};
|
||||
}]);
|
@ -1,164 +0,0 @@
|
||||
/*
|
||||
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 where in the event stream the web client currently is,
|
||||
repolling the event stream, and provides methods to resume/pause/stop the event
|
||||
stream. This service is not responsible for parsing event data. For that, see
|
||||
the eventHandlerService.
|
||||
*/
|
||||
angular.module('eventStreamService', [])
|
||||
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
|
||||
var END = "END";
|
||||
var SERVER_TIMEOUT_MS = 30000;
|
||||
var CLIENT_TIMEOUT_MS = 40000;
|
||||
var ERR_TIMEOUT_MS = 5000;
|
||||
|
||||
var settings = {
|
||||
from: "END",
|
||||
to: undefined,
|
||||
limit: undefined,
|
||||
shouldPoll: true,
|
||||
isActive: false
|
||||
};
|
||||
|
||||
// interrupts the stream. Only valid if there is a stream conneciton
|
||||
// open.
|
||||
var interrupt = function(shouldPoll) {
|
||||
console.log("[EventStream] interrupt("+shouldPoll+") "+
|
||||
JSON.stringify(settings));
|
||||
settings.shouldPoll = shouldPoll;
|
||||
settings.isActive = false;
|
||||
};
|
||||
|
||||
var saveStreamSettings = function() {
|
||||
localStorage.setItem("streamSettings", JSON.stringify(settings));
|
||||
};
|
||||
|
||||
var doEventStream = function(deferred) {
|
||||
settings.shouldPoll = true;
|
||||
settings.isActive = true;
|
||||
deferred = deferred || $q.defer();
|
||||
|
||||
// run the stream from the latest token
|
||||
matrixService.getEventStream(settings.from, SERVER_TIMEOUT_MS, CLIENT_TIMEOUT_MS).then(
|
||||
function(response) {
|
||||
if (!settings.isActive) {
|
||||
console.log("[EventStream] Got response but now inactive. Dropping data.");
|
||||
return;
|
||||
}
|
||||
|
||||
settings.from = response.data.end;
|
||||
|
||||
console.log(
|
||||
"[EventStream] Got response from "+settings.from+
|
||||
" to "+response.data.end
|
||||
);
|
||||
eventHandlerService.handleEvents(response.data.chunk, true);
|
||||
|
||||
deferred.resolve(response);
|
||||
|
||||
if (settings.shouldPoll) {
|
||||
$timeout(doEventStream, 0);
|
||||
}
|
||||
else {
|
||||
console.log("[EventStream] Stopping poll.");
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
if (error.status === 403) {
|
||||
settings.shouldPoll = false;
|
||||
}
|
||||
|
||||
deferred.reject(error);
|
||||
|
||||
if (settings.shouldPoll) {
|
||||
$timeout(doEventStream, ERR_TIMEOUT_MS);
|
||||
}
|
||||
else {
|
||||
console.log("[EventStream] Stopping polling.");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
var startEventStream = function() {
|
||||
settings.shouldPoll = true;
|
||||
settings.isActive = true;
|
||||
var deferred = $q.defer();
|
||||
|
||||
// Initial sync: get all information and the last 30 messages of all rooms of the user
|
||||
// 30 messages should be enough to display a full page of messages in a room
|
||||
// without requiring to make an additional request
|
||||
matrixService.initialSync(30, false).then(
|
||||
function(response) {
|
||||
eventHandlerService.handleInitialSyncDone(response);
|
||||
|
||||
// Start event streaming from that point
|
||||
settings.from = response.data.end;
|
||||
doEventStream(deferred);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return {
|
||||
// expose these values for testing
|
||||
SERVER_TIMEOUT: SERVER_TIMEOUT_MS,
|
||||
CLIENT_TIMEOUT: CLIENT_TIMEOUT_MS,
|
||||
|
||||
// resume the stream from whereever it last got up to. Typically used
|
||||
// when the page is opened.
|
||||
resume: function() {
|
||||
if (settings.isActive) {
|
||||
console.log("[EventStream] Already active, ignoring resume()");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[EventStream] resume "+JSON.stringify(settings));
|
||||
return startEventStream();
|
||||
},
|
||||
|
||||
// pause the stream. Resuming it will continue from the current position
|
||||
pause: function() {
|
||||
console.log("[EventStream] pause "+JSON.stringify(settings));
|
||||
// kill any running stream
|
||||
interrupt(false);
|
||||
// save the latest token
|
||||
saveStreamSettings();
|
||||
},
|
||||
|
||||
// stop the stream and wipe the position in the stream. Typically used
|
||||
// when logging out / logged out.
|
||||
stop: function() {
|
||||
console.log("[EventStream] stop "+JSON.stringify(settings));
|
||||
// kill any running stream
|
||||
interrupt(false);
|
||||
// clear the latest token
|
||||
settings.from = END;
|
||||
saveStreamSettings();
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
@ -1,659 +0,0 @@
|
||||
/*
|
||||
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';
|
||||
|
||||
var forAllVideoTracksOnStream = function(s, f) {
|
||||
var tracks = s.getVideoTracks();
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
f(tracks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var forAllAudioTracksOnStream = function(s, f) {
|
||||
var tracks = s.getAudioTracks();
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
f(tracks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var forAllTracksOnStream = function(s, f) {
|
||||
forAllVideoTracksOnStream(s, f);
|
||||
forAllAudioTracksOnStream(s, f);
|
||||
}
|
||||
|
||||
angular.module('MatrixCall', [])
|
||||
.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;
|
||||
this.call_id = "c" + new Date().getTime();
|
||||
this.state = 'fledgling';
|
||||
this.didConnect = false;
|
||||
|
||||
// a queue for candidates waiting to go out. We try to amalgamate candidates into a single candidate message where possible
|
||||
this.candidateSendQueue = [];
|
||||
this.candidateSendTries = 0;
|
||||
|
||||
var self = this;
|
||||
$rootScope.$watch(this.getRemoteVideoElement(), function (oldValue, newValue) {
|
||||
self.tryPlayRemoteStream();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
MatrixCall.getTurnServer = function() {
|
||||
matrixService.getTurnServer().then(function(response) {
|
||||
if (response.data.uris) {
|
||||
console.log("Got TURN URIs: "+response.data.uris);
|
||||
MatrixCall.turnServer = response.data;
|
||||
$rootScope.haveTurn = true;
|
||||
// re-fetch when we're about to reach the TTL
|
||||
$timeout(MatrixCall.getTurnServer, MatrixCall.turnServer.ttl * 1000 * 0.9);
|
||||
} else {
|
||||
console.log("Got no TURN URIs from HS");
|
||||
$rootScope.haveTurn = false;
|
||||
}
|
||||
}, function(error) {
|
||||
console.log("Failed to get TURN URIs");
|
||||
MatrixCall.turnServer = {};
|
||||
$timeout(MatrixCall.getTurnServer, 60000);
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: we should prevent any calls from being placed or accepted before this has finished
|
||||
MatrixCall.getTurnServer();
|
||||
|
||||
MatrixCall.CALL_TIMEOUT = 60000;
|
||||
MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302';
|
||||
|
||||
MatrixCall.prototype.createPeerConnection = function() {
|
||||
var pc;
|
||||
if (window.mozRTCPeerConnection) {
|
||||
var iceServers = [];
|
||||
// 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({
|
||||
'url': MatrixCall.turnServer.uris[i],
|
||||
'username': MatrixCall.turnServer.username,
|
||||
'credential': MatrixCall.turnServer.password,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log("No TURN server: using fallback STUN server");
|
||||
iceServers.push({ 'url' : MatrixCall.FALLBACK_STUN_SERVER });
|
||||
}
|
||||
}
|
||||
|
||||
pc = new window.mozRTCPeerConnection({"iceServers":iceServers});
|
||||
} else {
|
||||
var iceServers = [];
|
||||
// https://github.com/EricssonResearch/openwebrtc/issues/85
|
||||
if (MatrixCall.turnServer && !this.isOpenWebRTC()) {
|
||||
if (MatrixCall.turnServer.uris) {
|
||||
iceServers.push({
|
||||
'urls': MatrixCall.turnServer.uris,
|
||||
'username': MatrixCall.turnServer.username,
|
||||
'credential': MatrixCall.turnServer.password,
|
||||
});
|
||||
} else {
|
||||
console.log("No TURN server: using fallback STUN server");
|
||||
iceServers.push({ 'urls' : MatrixCall.FALLBACK_STUN_SERVER });
|
||||
}
|
||||
}
|
||||
|
||||
pc = new window.RTCPeerConnection({"iceServers":iceServers});
|
||||
}
|
||||
var self = this;
|
||||
pc.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
|
||||
pc.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
|
||||
pc.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
|
||||
pc.onaddstream = function(s) { self.onAddStream(s); };
|
||||
return pc;
|
||||
}
|
||||
|
||||
MatrixCall.prototype.getUserMediaVideoContraints = function(callType) {
|
||||
switch (callType) {
|
||||
case 'voice':
|
||||
return ({audio: true, video: false});
|
||||
case 'video':
|
||||
return ({audio: true, video: {
|
||||
mandatory: {
|
||||
minWidth: 640,
|
||||
maxWidth: 640,
|
||||
minHeight: 360,
|
||||
maxHeight: 360,
|
||||
}
|
||||
}});
|
||||
}
|
||||
};
|
||||
|
||||
MatrixCall.prototype.placeVoiceCall = function() {
|
||||
this.placeCallWithConstraints(this.getUserMediaVideoContraints('voice'));
|
||||
this.type = 'voice';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.placeVideoCall = function(config) {
|
||||
this.placeCallWithConstraints(this.getUserMediaVideoContraints('video'));
|
||||
this.type = 'video';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.placeCallWithConstraints = function(constraints) {
|
||||
var self = this;
|
||||
matrixPhoneService.callPlaced(this);
|
||||
navigator.getUserMedia(constraints, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); });
|
||||
this.state = 'wait_local_media';
|
||||
this.direction = 'outbound';
|
||||
this.config = constraints;
|
||||
};
|
||||
|
||||
MatrixCall.prototype.initWithInvite = function(event) {
|
||||
this.msg = event.content;
|
||||
this.peerConn = this.createPeerConnection();
|
||||
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
|
||||
this.state = 'ringing';
|
||||
this.direction = 'inbound';
|
||||
|
||||
// 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) {
|
||||
this.type = 'video';
|
||||
} else {
|
||||
this.type = 'voice';
|
||||
}
|
||||
//}
|
||||
|
||||
var self = this;
|
||||
$timeout(function() {
|
||||
if (self.state == 'ringing') {
|
||||
self.state = 'ended';
|
||||
self.hangupParty = 'remote'; // effectively
|
||||
self.stopAllMedia();
|
||||
if (self.peerConn.signalingState != 'closed') self.peerConn.close();
|
||||
if (self.onHangup) self.onHangup(self);
|
||||
}
|
||||
}, this.msg.lifetime - event.age);
|
||||
};
|
||||
|
||||
// perverse as it may seem, sometimes we want to instantiate a call with a hangup message
|
||||
// (because when getting the state of the room on load, events come in reverse order and
|
||||
// we want to remember that a call has been hung up)
|
||||
MatrixCall.prototype.initWithHangup = function(event) {
|
||||
this.msg = event.content;
|
||||
this.state = 'ended';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.answer = function() {
|
||||
console.log("Answering call "+this.call_id);
|
||||
|
||||
var self = this;
|
||||
|
||||
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();
|
||||
}, function() {
|
||||
console.log("Failed to join room: can't answer call!");
|
||||
self.onError("Unable to join room to answer call!");
|
||||
self.hangup();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.localAVStream && !this.waitForLocalAVStream) {
|
||||
navigator.getUserMedia(this.getUserMediaVideoContraints(this.type), function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); });
|
||||
this.state = 'wait_local_media';
|
||||
} else if (this.localAVStream) {
|
||||
this.gotUserMediaForAnswer(this.localAVStream);
|
||||
} else if (this.waitForLocalAVStream) {
|
||||
this.state = 'wait_local_media';
|
||||
}
|
||||
};
|
||||
|
||||
MatrixCall.prototype.stopAllMedia = function() {
|
||||
if (this.localAVStream) {
|
||||
forAllTracksOnStream(this.localAVStream, function(t) {
|
||||
if (t.stop) t.stop();
|
||||
});
|
||||
}
|
||||
if (this.remoteAVStream) {
|
||||
forAllTracksOnStream(this.remoteAVStream, function(t) {
|
||||
if (t.stop) t.stop();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MatrixCall.prototype.hangup = function(reason, suppressEvent) {
|
||||
console.log("Ending call "+this.call_id);
|
||||
|
||||
// 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.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause();
|
||||
if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().pause();
|
||||
|
||||
this.stopAllMedia();
|
||||
if (this.peerConn) this.peerConn.close();
|
||||
|
||||
this.hangupParty = 'local';
|
||||
this.hangupReason = reason;
|
||||
|
||||
var content = {
|
||||
version: 0,
|
||||
call_id: this.call_id,
|
||||
reason: reason
|
||||
};
|
||||
this.sendEventWithRetry('m.call.hangup', content);
|
||||
this.state = 'ended';
|
||||
if (this.onHangup && !suppressEvent) this.onHangup(this);
|
||||
};
|
||||
|
||||
MatrixCall.prototype.gotUserMediaForInvite = function(stream) {
|
||||
if (this.successor) {
|
||||
this.successor.gotUserMediaForAnswer(stream);
|
||||
return;
|
||||
}
|
||||
if (this.state == 'ended') return;
|
||||
|
||||
var videoEl = this.getLocalVideoElement();
|
||||
|
||||
if (videoEl && this.type == 'video') {
|
||||
var vidTrack = stream.getVideoTracks()[0];
|
||||
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;
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioTracks[i].enabled = true;
|
||||
}
|
||||
this.peerConn = this.createPeerConnection();
|
||||
this.peerConn.addStream(stream);
|
||||
var self = this;
|
||||
this.peerConn.createOffer(function(d) {
|
||||
self.gotLocalOffer(d);
|
||||
}, function(e) {
|
||||
self.getLocalOfferFailed(e);
|
||||
});
|
||||
$rootScope.$apply(function() {
|
||||
self.state = 'create_offer';
|
||||
});
|
||||
};
|
||||
|
||||
MatrixCall.prototype.gotUserMediaForAnswer = function(stream) {
|
||||
if (this.state == 'ended') return;
|
||||
|
||||
var localVidEl = this.getLocalVideoElement();
|
||||
|
||||
if (localVidEl && this.type == 'video') {
|
||||
localVidEl.autoplay = true;
|
||||
var vidTrack = stream.getVideoTracks()[0];
|
||||
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;
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioTracks[i].enabled = true;
|
||||
}
|
||||
this.peerConn.addStream(stream);
|
||||
var self = this;
|
||||
var constraints = {
|
||||
'mandatory': {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': this.type == 'video'
|
||||
},
|
||||
};
|
||||
this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints);
|
||||
// This can't be in an apply() because it's called by a predecessor call under glare conditions :(
|
||||
self.state = 'create_answer';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.gotLocalIceCandidate = function(event) {
|
||||
if (event.candidate) {
|
||||
console.log("Got local ICE "+event.candidate.sdpMid+" candidate: "+event.candidate.candidate);
|
||||
this.sendCandidate(event.candidate);
|
||||
}
|
||||
}
|
||||
|
||||
MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
|
||||
if (this.state == '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) {});
|
||||
};
|
||||
|
||||
MatrixCall.prototype.receivedAnswer = function(msg) {
|
||||
if (this.state == 'ended') return;
|
||||
|
||||
this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
|
||||
this.state = 'connecting';
|
||||
};
|
||||
|
||||
|
||||
MatrixCall.prototype.gotLocalOffer = function(description) {
|
||||
console.log("Created offer: "+description);
|
||||
|
||||
if (this.state == 'ended') {
|
||||
console.log("Ignoring newly created offer on call ID "+this.call_id+" because the call has ended");
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
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';
|
||||
});
|
||||
}, function() { console.log("Error setting local description!"); });
|
||||
};
|
||||
|
||||
MatrixCall.prototype.createdAnswer = function(description) {
|
||||
console.log("Created answer: "+description);
|
||||
var self = this;
|
||||
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) {
|
||||
this.onError("Failed to start audio for call!");
|
||||
};
|
||||
|
||||
MatrixCall.prototype.getUserMediaFailed = function() {
|
||||
this.onError("Couldn't start capturing! Is your microphone set up?");
|
||||
this.hangup();
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onIceConnectionStateChanged = function() {
|
||||
if (this.state == 'ended') return; // because ICE can still complete as we're ending the call
|
||||
console.log("Ice connection state changed to: "+this.peerConn.iceConnectionState);
|
||||
// ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
|
||||
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
|
||||
var self = this;
|
||||
$rootScope.$apply(function() {
|
||||
self.state = 'connected';
|
||||
self.didConnect = true;
|
||||
});
|
||||
} else if (this.peerConn.iceConnectionState == 'failed') {
|
||||
this.hangup('ice_failed');
|
||||
}
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onSignallingStateChanged = function() {
|
||||
console.log("call "+this.call_id+": Signalling state changed to: "+this.peerConn.signalingState);
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onSetRemoteDescriptionSuccess = function() {
|
||||
console.log("Set remote description");
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onSetRemoteDescriptionError = function(e) {
|
||||
console.log("Failed to set remote description"+e);
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onAddStream = function(event) {
|
||||
console.log("Stream added"+event);
|
||||
|
||||
var s = event.stream;
|
||||
|
||||
this.remoteAVStream = s;
|
||||
|
||||
if (this.direction == 'inbound') {
|
||||
if (s.getVideoTracks().length > 0) {
|
||||
this.type = 'video';
|
||||
} else {
|
||||
this.type = 'voice';
|
||||
}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
forAllTracksOnStream(s, function(t) {
|
||||
// not currently implemented in chrome
|
||||
t.onstarted = self.onRemoteStreamTrackStarted;
|
||||
});
|
||||
|
||||
event.stream.onended = function(e) { self.onRemoteStreamEnded(e); };
|
||||
// not currently implemented in chrome
|
||||
event.stream.onstarted = function(e) { self.onRemoteStreamStarted(e); };
|
||||
|
||||
this.tryPlayRemoteStream();
|
||||
};
|
||||
|
||||
MatrixCall.prototype.tryPlayRemoteStream = function(event) {
|
||||
if (this.getRemoteVideoElement() && this.remoteAVStream) {
|
||||
var player = this.getRemoteVideoElement();
|
||||
player.autoplay = true;
|
||||
player.src = URL.createObjectURL(this.remoteAVStream);
|
||||
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';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onRemoteStreamStarted = function(event) {
|
||||
var self = this;
|
||||
$rootScope.$apply(function() {
|
||||
self.state = 'connected';
|
||||
});
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onRemoteStreamEnded = function(event) {
|
||||
console.log("Remote stream ended");
|
||||
var self = this;
|
||||
$rootScope.$apply(function() {
|
||||
self.state = 'ended';
|
||||
self.hangupParty = 'remote';
|
||||
self.stopAllMedia();
|
||||
if (self.peerConn.signalingState != 'closed') self.peerConn.close();
|
||||
if (self.onHangup) self.onHangup(self);
|
||||
});
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) {
|
||||
var self = this;
|
||||
$rootScope.$apply(function() {
|
||||
self.state = 'connected';
|
||||
});
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onHangupReceived = function(msg) {
|
||||
console.log("Hangup received");
|
||||
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;
|
||||
this.stopAllMedia();
|
||||
if (this.peerConn && this.peerConn.signalingState != 'closed') this.peerConn.close();
|
||||
if (this.onHangup) this.onHangup(this);
|
||||
};
|
||||
|
||||
MatrixCall.prototype.replacedBy = function(newCall) {
|
||||
console.log(this.call_id+" being replaced by "+newCall.call_id);
|
||||
if (this.state == 'wait_local_media') {
|
||||
console.log("Telling new call to wait for local media");
|
||||
newCall.waitForLocalAVStream = true;
|
||||
} else if (this.state == 'create_offer') {
|
||||
console.log("Handing local stream to new call");
|
||||
newCall.gotUserMediaForAnswer(this.localAVStream);
|
||||
delete(this.localAVStream);
|
||||
} else if (this.state == 'invite_sent') {
|
||||
console.log("Handing local stream to new call");
|
||||
newCall.gotUserMediaForAnswer(this.localAVStream);
|
||||
delete(this.localAVStream);
|
||||
}
|
||||
newCall.localVideoSelector = this.localVideoSelector;
|
||||
newCall.remoteVideoSelector = this.remoteVideoSelector;
|
||||
this.successor = newCall;
|
||||
this.hangup(true);
|
||||
};
|
||||
|
||||
MatrixCall.prototype.sendEventWithRetry = function(evType, content) {
|
||||
var ev = { type:evType, content:content, tries:1 };
|
||||
var self = this;
|
||||
matrixService.sendEvent(this.room_id, evType, undefined, content).then(this.eventSent, function(error) { self.eventSendFailed(ev, error); } );
|
||||
};
|
||||
|
||||
MatrixCall.prototype.eventSent = function() {
|
||||
};
|
||||
|
||||
MatrixCall.prototype.eventSendFailed = function(ev, error) {
|
||||
if (ev.tries > 5) {
|
||||
console.log("Failed to send event of type "+ev.type+" on attempt "+ev.tries+". Giving up.");
|
||||
return;
|
||||
}
|
||||
var delayMs = 500 * Math.pow(2, ev.tries);
|
||||
console.log("Failed to send event of type "+ev.type+". Retrying in "+delayMs+"ms");
|
||||
++ev.tries;
|
||||
var self = this;
|
||||
$timeout(function() {
|
||||
matrixService.sendEvent(self.room_id, ev.type, undefined, ev.content).then(self.eventSent, function(error) { self.eventSendFailed(ev, error); } );
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
// Sends candidates with are sent in a special way because we try to amalgamate them into one message
|
||||
MatrixCall.prototype.sendCandidate = function(content) {
|
||||
this.candidateSendQueue.push(content);
|
||||
var self = this;
|
||||
if (this.candidateSendTries == 0) $timeout(function() { self.sendCandidateQueue(); }, 100);
|
||||
};
|
||||
|
||||
MatrixCall.prototype.sendCandidateQueue = function(content) {
|
||||
if (this.candidateSendQueue.length == 0) return;
|
||||
|
||||
var cands = this.candidateSendQueue;
|
||||
this.candidateSendQueue = [];
|
||||
++this.candidateSendTries;
|
||||
var content = {
|
||||
version: 0,
|
||||
call_id: this.call_id,
|
||||
candidates: cands
|
||||
};
|
||||
var self = this;
|
||||
console.log("Attempting to send "+cands.length+" candidates");
|
||||
matrixService.sendEvent(self.room_id, 'm.call.candidates', undefined, content).then(function() { self.candsSent(); }, function(error) { self.candsSendFailed(cands, error); } );
|
||||
};
|
||||
|
||||
MatrixCall.prototype.candsSent = function() {
|
||||
this.candidateSendTries = 0;
|
||||
this.sendCandidateQueue();
|
||||
};
|
||||
|
||||
MatrixCall.prototype.candsSendFailed = function(cands, error) {
|
||||
for (var i = 0; i < cands.length; ++i) {
|
||||
this.candidateSendQueue.push(cands[i]);
|
||||
}
|
||||
|
||||
if (this.candidateSendTries > 5) {
|
||||
console.log("Failed to send candidates on attempt "+this.candidateSendTries+". Giving up for now.");
|
||||
this.candidateSendTries = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var delayMs = 500 * Math.pow(2, this.candidateSendTries);
|
||||
++this.candidateSendTries;
|
||||
console.log("Failed to send candidates. Retrying in "+delayMs+"ms");
|
||||
var self = this;
|
||||
$timeout(function() {
|
||||
self.sendCandidateQueue();
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
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;
|
||||
}]);
|
@ -1,172 +0,0 @@
|
||||
/*
|
||||
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;
|
||||
};
|
||||
}]);
|
@ -1,155 +0,0 @@
|
||||
/*
|
||||
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('matrixPhoneService', [])
|
||||
.factory('matrixPhoneService', ['$rootScope', '$injector', 'matrixService', 'eventHandlerService', function MatrixPhoneService($rootScope, $injector, matrixService, eventHandlerService) {
|
||||
var matrixPhoneService = function() {
|
||||
};
|
||||
|
||||
matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
|
||||
matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT";
|
||||
matrixPhoneService.allCalls = {};
|
||||
// a place to save candidates that come in for calls we haven't got invites for yet (when paginating backwards)
|
||||
matrixPhoneService.candidatesByCall = {};
|
||||
|
||||
matrixPhoneService.callPlaced = function(call) {
|
||||
matrixPhoneService.allCalls[call.call_id] = call;
|
||||
};
|
||||
|
||||
$rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
|
||||
if (event.user_id == matrixService.config().user_id) return;
|
||||
|
||||
var msg = event.content;
|
||||
|
||||
if (event.type == 'm.call.invite') {
|
||||
if (event.age == undefined || msg.lifetime == undefined) {
|
||||
// if the event doesn't have either an age (the HS is too old) or a lifetime
|
||||
// (the sending client was too old when it sent it) then fall back to old behaviour
|
||||
if (!isLive) return; // until matrix supports expiring messages
|
||||
}
|
||||
|
||||
if (event.age > msg.lifetime) {
|
||||
console.log("Ignoring expired call event of type "+event.type);
|
||||
return;
|
||||
}
|
||||
|
||||
var call = undefined;
|
||||
if (!isLive) {
|
||||
// if this event wasn't live then this call may already be over
|
||||
call = matrixPhoneService.allCalls[msg.call_id];
|
||||
if (call && call.state == 'ended') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var MatrixCall = $injector.get('MatrixCall');
|
||||
var call = new MatrixCall(event.room_id);
|
||||
|
||||
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.
|
||||
// instead, we broadcast a fake call event with a non-functional call object
|
||||
$rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
|
||||
return;
|
||||
}
|
||||
|
||||
call.call_id = msg.call_id;
|
||||
call.initWithInvite(event);
|
||||
matrixPhoneService.allCalls[call.call_id] = call;
|
||||
|
||||
// if we stashed candidate events for that call ID, play them back now
|
||||
if (!isLive && matrixPhoneService.candidatesByCall[call.call_id] != undefined) {
|
||||
for (var i = 0; i < matrixPhoneService.candidatesByCall[call.call_id].length; ++i) {
|
||||
call.gotRemoteIceCandidate(matrixPhoneService.candidatesByCall[call.call_id][i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Were we trying to call that user (room)?
|
||||
var existingCall;
|
||||
var callIds = Object.keys(matrixPhoneService.allCalls);
|
||||
for (var i = 0; i < callIds.length; ++i) {
|
||||
var thisCallId = callIds[i];
|
||||
var thisCall = matrixPhoneService.allCalls[thisCallId];
|
||||
|
||||
if (call.room_id == thisCall.room_id && thisCall.direction == 'outbound'
|
||||
&& (thisCall.state == 'wait_local_media' || thisCall.state == 'create_offer' || thisCall.state == 'invite_sent')) {
|
||||
existingCall = thisCall;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingCall) {
|
||||
// If we've only got to wait_local_media or create_offer and we've got an invite,
|
||||
// pick the incoming call because we know we haven't sent our invite yet
|
||||
// otherwise, pick whichever call has the lowest call ID (by string comparison)
|
||||
if (existingCall.state == 'wait_local_media' || existingCall.state == 'create_offer' || existingCall.call_id > call.call_id) {
|
||||
console.log("Glare detected: answering incoming call "+call.call_id+" and canceling outgoing call "+existingCall.call_id);
|
||||
existingCall.replacedBy(call);
|
||||
call.answer();
|
||||
$rootScope.$broadcast(matrixPhoneService.REPLACED_CALL_EVENT, existingCall, call);
|
||||
} else {
|
||||
console.log("Glare detected: rejecting incoming call "+call.call_id+" and keeping outgoing call "+existingCall.call_id);
|
||||
call.hangup();
|
||||
}
|
||||
} else {
|
||||
$rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
|
||||
}
|
||||
} else if (event.type == 'm.call.answer') {
|
||||
var call = matrixPhoneService.allCalls[msg.call_id];
|
||||
if (!call) {
|
||||
console.log("Got answer for unknown call ID "+msg.call_id);
|
||||
return;
|
||||
}
|
||||
call.receivedAnswer(msg);
|
||||
} else if (event.type == 'm.call.candidates') {
|
||||
var call = matrixPhoneService.allCalls[msg.call_id];
|
||||
if (!call && isLive) {
|
||||
console.log("Got candidates for unknown call ID "+msg.call_id);
|
||||
return;
|
||||
} else if (!call) {
|
||||
if (matrixPhoneService.candidatesByCall[msg.call_id] == undefined) {
|
||||
matrixPhoneService.candidatesByCall[msg.call_id] = [];
|
||||
}
|
||||
matrixPhoneService.candidatesByCall[msg.call_id] = matrixPhoneService.candidatesByCall[msg.call_id].concat(msg.candidates);
|
||||
} else {
|
||||
for (var i = 0; i < msg.candidates.length; ++i) {
|
||||
call.gotRemoteIceCandidate(msg.candidates[i]);
|
||||
}
|
||||
}
|
||||
} else if (event.type == 'm.call.hangup') {
|
||||
var call = matrixPhoneService.allCalls[msg.call_id];
|
||||
if (!call && isLive) {
|
||||
console.log("Got hangup for unknown call ID "+msg.call_id);
|
||||
} else if (!call) {
|
||||
// if not live, store the fact that the call has ended because we're probably getting events backwards so
|
||||
// the hangup will come before the invite
|
||||
var MatrixCall = $injector.get('MatrixCall');
|
||||
var call = new MatrixCall(event.room_id);
|
||||
call.call_id = msg.call_id;
|
||||
call.initWithHangup(event);
|
||||
matrixPhoneService.allCalls[msg.call_id] = call;
|
||||
} else {
|
||||
call.onHangupReceived(msg);
|
||||
delete(matrixPhoneService.allCalls[msg.call_id]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return matrixPhoneService;
|
||||
}]);
|
@ -1,701 +0,0 @@
|
||||
/*
|
||||
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 wraps up Matrix API calls.
|
||||
|
||||
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', function($http, $q) {
|
||||
|
||||
/*
|
||||
* Permanent storage of user information
|
||||
* The config contains:
|
||||
* - homeserver url
|
||||
* - Identity server url
|
||||
* - user_id
|
||||
* - access_token
|
||||
* - version: the version of this cache
|
||||
*/
|
||||
var config;
|
||||
|
||||
// Current version of permanent storage
|
||||
var configVersion = 0;
|
||||
var prefixPath = "/_matrix/client/api/v1";
|
||||
|
||||
var doRequest = function(method, path, params, data, $httpParams) {
|
||||
if (!config) {
|
||||
console.warn("No config exists. Cannot perform request to "+path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject the access token
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
|
||||
params.access_token = config.access_token;
|
||||
|
||||
if (path.indexOf(prefixPath) !== 0) {
|
||||
path = prefixPath + path;
|
||||
}
|
||||
|
||||
return doBaseRequest(config.homeserver, method, path, params, data, undefined, $httpParams);
|
||||
};
|
||||
|
||||
var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
|
||||
|
||||
var request = {
|
||||
method: method,
|
||||
url: baseUrl + path,
|
||||
params: params,
|
||||
data: data,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
// Add additional $http parameters
|
||||
if ($httpParams) {
|
||||
angular.extend(request, $httpParams);
|
||||
}
|
||||
|
||||
return $http(request);
|
||||
};
|
||||
|
||||
var doRegisterLogin = function(path, loginType, sessionId, userName, password, threepidCreds) {
|
||||
var data = {};
|
||||
if (loginType === "m.login.recaptcha") {
|
||||
var challengeToken = Recaptcha.get_challenge();
|
||||
var captchaEntry = Recaptcha.get_response();
|
||||
data = {
|
||||
type: "m.login.recaptcha",
|
||||
challenge: challengeToken,
|
||||
response: captchaEntry
|
||||
};
|
||||
}
|
||||
else if (loginType === "m.login.email.identity") {
|
||||
data = {
|
||||
threepidCreds: threepidCreds
|
||||
};
|
||||
}
|
||||
else if (loginType === "m.login.password") {
|
||||
data = {
|
||||
user: userName,
|
||||
password: password
|
||||
};
|
||||
}
|
||||
|
||||
if (sessionId) {
|
||||
data.session = sessionId;
|
||||
}
|
||||
data.type = loginType;
|
||||
console.log("doRegisterLogin >>> " + loginType);
|
||||
return doRequest("POST", path, undefined, data);
|
||||
};
|
||||
|
||||
return {
|
||||
/****** Home server API ******/
|
||||
prefix: prefixPath,
|
||||
|
||||
// Register an user
|
||||
register: function(user_name, password, threepidCreds, useCaptcha) {
|
||||
// registration is composed of multiple requests, to check you can
|
||||
// register, then to actually register. This deferred will fire when
|
||||
// all the requests are done, along with the final response.
|
||||
var deferred = $q.defer();
|
||||
var path = "/register";
|
||||
|
||||
// check we can actually register with this HS.
|
||||
doRequest("GET", path, undefined, undefined).then(
|
||||
function(response) {
|
||||
console.log("/register [1] : "+JSON.stringify(response));
|
||||
var flows = response.data.flows;
|
||||
var knownTypes = [
|
||||
"m.login.password",
|
||||
"m.login.recaptcha",
|
||||
"m.login.email.identity"
|
||||
];
|
||||
// if they entered 3pid creds, we want to use a flow which uses it.
|
||||
var useThreePidFlow = threepidCreds != undefined;
|
||||
var flowIndex = 0;
|
||||
var firstRegType = undefined;
|
||||
|
||||
for (var i=0; i<flows.length; i++) {
|
||||
var isThreePidFlow = false;
|
||||
if (flows[i].stages) {
|
||||
for (var j=0; j<flows[i].stages.length; j++) {
|
||||
var regType = flows[i].stages[j];
|
||||
if (knownTypes.indexOf(regType) === -1) {
|
||||
deferred.reject("Unknown type: "+regType);
|
||||
return;
|
||||
}
|
||||
if (regType == "m.login.email.identity") {
|
||||
isThreePidFlow = true;
|
||||
}
|
||||
if (!useCaptcha && regType == "m.login.recaptcha") {
|
||||
console.error("Web client setup to not use captcha, but HS demands a captcha.");
|
||||
deferred.reject({
|
||||
data: {
|
||||
errcode: "M_CAPTCHA_NEEDED",
|
||||
error: "Home server requires a captcha."
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( (isThreePidFlow && useThreePidFlow) || (!isThreePidFlow && !useThreePidFlow) ) {
|
||||
flowIndex = i;
|
||||
}
|
||||
|
||||
if (knownTypes.indexOf(flows[i].type) == -1) {
|
||||
deferred.reject("Unknown type: "+flows[i].type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// looks like we can register fine, go ahead and do it.
|
||||
console.log("Using flow " + JSON.stringify(flows[flowIndex]));
|
||||
firstRegType = flows[flowIndex].type;
|
||||
var sessionId = undefined;
|
||||
|
||||
// generic response processor so it can loop as many times as required
|
||||
var loginResponseFunc = function(response) {
|
||||
if (response.data.session) {
|
||||
sessionId = response.data.session;
|
||||
}
|
||||
console.log("login response: " + JSON.stringify(response.data));
|
||||
if (response.data.access_token) {
|
||||
deferred.resolve(response);
|
||||
}
|
||||
else if (response.data.next) {
|
||||
var nextType = response.data.next;
|
||||
if (response.data.next instanceof Array) {
|
||||
for (var i=0; i<response.data.next.length; i++) {
|
||||
if (useThreePidFlow && response.data.next[i] == "m.login.email.identity") {
|
||||
nextType = response.data.next[i];
|
||||
break;
|
||||
}
|
||||
else if (!useThreePidFlow && response.data.next[i] != "m.login.email.identity") {
|
||||
nextType = response.data.next[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return doRegisterLogin(path, nextType, sessionId, user_name, password, threepidCreds).then(
|
||||
loginResponseFunc,
|
||||
function(err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
deferred.reject("Unknown continuation: "+JSON.stringify(response));
|
||||
}
|
||||
};
|
||||
|
||||
// set the ball rolling
|
||||
doRegisterLogin(path, firstRegType, undefined, user_name, password, threepidCreds).then(
|
||||
loginResponseFunc,
|
||||
function(err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
function(err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
// Create a room
|
||||
create: function(room_alias, visibility) {
|
||||
// The REST path spec
|
||||
var path = "/createRoom";
|
||||
|
||||
var req = {
|
||||
"visibility": visibility
|
||||
};
|
||||
if (room_alias) {
|
||||
req.room_alias_name = room_alias;
|
||||
}
|
||||
|
||||
return doRequest("POST", path, undefined, req);
|
||||
},
|
||||
|
||||
// Get the user's current state: his presence, the list of his rooms with
|
||||
// the last {limit} events
|
||||
initialSync: function(limit, feedback) {
|
||||
// The REST path spec
|
||||
|
||||
var path = "/initialSync";
|
||||
|
||||
var params = {};
|
||||
if (limit) {
|
||||
params.limit = limit;
|
||||
}
|
||||
if (feedback) {
|
||||
params.feedback = feedback;
|
||||
}
|
||||
|
||||
return doRequest("GET", path, params);
|
||||
},
|
||||
|
||||
// get room state for a specific room
|
||||
roomState: function(room_id) {
|
||||
var path = "/rooms/" + encodeURIComponent(room_id) + "/state";
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
// Joins a room
|
||||
join: function(room_id) {
|
||||
return this.membershipChange(room_id, undefined, "join");
|
||||
},
|
||||
|
||||
joinAlias: function(room_alias) {
|
||||
var path = "/join/$room_alias";
|
||||
room_alias = encodeURIComponent(room_alias);
|
||||
|
||||
path = path.replace("$room_alias", room_alias);
|
||||
|
||||
// TODO: PUT with txn ID
|
||||
return doRequest("POST", path, undefined, {});
|
||||
},
|
||||
|
||||
// Invite a user to a room
|
||||
invite: function(room_id, user_id) {
|
||||
return this.membershipChange(room_id, user_id, "invite");
|
||||
},
|
||||
|
||||
// Leaves a room
|
||||
leave: function(room_id) {
|
||||
return this.membershipChange(room_id, undefined, "leave");
|
||||
},
|
||||
|
||||
membershipChange: function(room_id, user_id, membershipValue) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/$membership";
|
||||
path = path.replace("$room_id", encodeURIComponent(room_id));
|
||||
path = path.replace("$membership", encodeURIComponent(membershipValue));
|
||||
|
||||
var data = {};
|
||||
if (user_id !== undefined) {
|
||||
data = { user_id: user_id };
|
||||
}
|
||||
|
||||
// TODO: Use PUT with transaction IDs
|
||||
return doRequest("POST", path, undefined, data);
|
||||
},
|
||||
|
||||
// Change the membership of an another user
|
||||
setMembership: function(room_id, user_id, membershipValue, reason) {
|
||||
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/state/m.room.member/$user_id";
|
||||
path = path.replace("$room_id", encodeURIComponent(room_id));
|
||||
path = path.replace("$user_id", user_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, {
|
||||
membership : membershipValue,
|
||||
reason: reason
|
||||
});
|
||||
},
|
||||
|
||||
// Bans a user from a room
|
||||
ban: function(room_id, user_id, reason) {
|
||||
var path = "/rooms/$room_id/ban";
|
||||
path = path.replace("$room_id", encodeURIComponent(room_id));
|
||||
|
||||
return doRequest("POST", path, undefined, {
|
||||
user_id: user_id,
|
||||
reason: reason
|
||||
});
|
||||
},
|
||||
|
||||
// Unbans a user in a room
|
||||
unban: function(room_id, user_id) {
|
||||
// FIXME: To update when there will be homeserver API for unban
|
||||
// For now, do an unban by resetting the user membership to "leave"
|
||||
return this.setMembership(room_id, user_id, "leave");
|
||||
},
|
||||
|
||||
// Kicks a user from a room
|
||||
kick: function(room_id, user_id, reason) {
|
||||
// Set the user membership to "leave" to kick him
|
||||
return this.setMembership(room_id, user_id, "leave", reason);
|
||||
},
|
||||
|
||||
// Retrieves the room ID corresponding to a room alias
|
||||
resolveRoomAlias:function(room_alias) {
|
||||
var path = "/_matrix/client/api/v1/directory/room/$room_alias";
|
||||
room_alias = encodeURIComponent(room_alias);
|
||||
|
||||
path = path.replace("$room_alias", room_alias);
|
||||
|
||||
return doRequest("GET", path, undefined, {});
|
||||
},
|
||||
|
||||
setName: function(room_id, name) {
|
||||
var data = {
|
||||
name: name
|
||||
};
|
||||
return this.sendStateEvent(room_id, "m.room.name", data);
|
||||
},
|
||||
|
||||
setTopic: function(room_id, topic) {
|
||||
var data = {
|
||||
topic: topic
|
||||
};
|
||||
return this.sendStateEvent(room_id, "m.room.topic", data);
|
||||
},
|
||||
|
||||
|
||||
sendStateEvent: function(room_id, eventType, content, state_key) {
|
||||
var path = "/rooms/$room_id/state/"+ eventType;
|
||||
// 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 += "/" + encodeURIComponent(state_key);
|
||||
}
|
||||
room_id = encodeURIComponent(room_id);
|
||||
path = path.replace("$room_id", room_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, content);
|
||||
},
|
||||
|
||||
sendEvent: function(room_id, eventType, txn_id, content) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/send/"+eventType+"/$txn_id";
|
||||
|
||||
if (!txn_id) {
|
||||
txn_id = "m" + new Date().getTime();
|
||||
}
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$txn_id", txn_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, content);
|
||||
},
|
||||
|
||||
sendMessage: function(room_id, txn_id, content) {
|
||||
return this.sendEvent(room_id, 'm.room.message', txn_id, content);
|
||||
},
|
||||
|
||||
// Send a text message
|
||||
sendTextMessage: function(room_id, body, msg_id) {
|
||||
var content = {
|
||||
msgtype: "m.text",
|
||||
body: body
|
||||
};
|
||||
|
||||
return this.sendMessage(room_id, msg_id, content);
|
||||
},
|
||||
|
||||
// Send an image message
|
||||
sendImageMessage: function(room_id, image_url, image_body, msg_id) {
|
||||
var content = {
|
||||
msgtype: "m.image",
|
||||
url: image_url,
|
||||
info: image_body,
|
||||
body: "Image"
|
||||
};
|
||||
|
||||
return this.sendMessage(room_id, msg_id, content);
|
||||
},
|
||||
|
||||
// Send an emote message
|
||||
sendEmoteMessage: function(room_id, body, msg_id) {
|
||||
var content = {
|
||||
msgtype: "m.emote",
|
||||
body: body
|
||||
};
|
||||
|
||||
return this.sendMessage(room_id, msg_id, content);
|
||||
},
|
||||
|
||||
redactEvent: function(room_id, event_id) {
|
||||
var path = "/rooms/$room_id/redact/$event_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);
|
||||
},
|
||||
|
||||
// get a snapshot of the members in a room.
|
||||
getMemberList: function(room_id) {
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
var path = "/rooms/$room_id/members";
|
||||
path = path.replace("$room_id", room_id);
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
paginateBackMessages: function(room_id, from_token, limit) {
|
||||
var path = "/rooms/$room_id/messages";
|
||||
path = path.replace("$room_id", encodeURIComponent(room_id));
|
||||
var params = {
|
||||
from: from_token,
|
||||
limit: limit,
|
||||
dir: 'b'
|
||||
};
|
||||
return doRequest("GET", path, params);
|
||||
},
|
||||
|
||||
// get a list of public rooms on your home server
|
||||
publicRooms: function() {
|
||||
var path = "/publicRooms";
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
// get a user's profile
|
||||
getProfile: function(userId) {
|
||||
return this.getProfileInfo(userId);
|
||||
},
|
||||
|
||||
// get a display name for this user ID
|
||||
getDisplayName: function(userId) {
|
||||
return this.getProfileInfo(userId, "displayname");
|
||||
},
|
||||
|
||||
// get the profile picture url for this user ID
|
||||
getProfilePictureUrl: function(userId) {
|
||||
return this.getProfileInfo(userId, "avatar_url");
|
||||
},
|
||||
|
||||
// update your display name
|
||||
setDisplayName: function(newName) {
|
||||
var content = {
|
||||
displayname: newName
|
||||
};
|
||||
return this.setProfileInfo(content, "displayname");
|
||||
},
|
||||
|
||||
// update your profile picture url
|
||||
setProfilePictureUrl: function(newUrl) {
|
||||
var content = {
|
||||
avatar_url: newUrl
|
||||
};
|
||||
return this.setProfileInfo(content, "avatar_url");
|
||||
},
|
||||
|
||||
setProfileInfo: function(data, info_segment) {
|
||||
var path = "/profile/$user/" + info_segment;
|
||||
path = path.replace("$user", encodeURIComponent(config.user_id));
|
||||
return doRequest("PUT", path, undefined, data);
|
||||
},
|
||||
|
||||
getProfileInfo: function(userId, info_segment) {
|
||||
var path = "/profile/"+encodeURIComponent(userId);
|
||||
if (info_segment) path += '/' + info_segment;
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
login: function(userId, password) {
|
||||
// TODO We should be checking to make sure the client can support
|
||||
// logging in to this HS, else use the fallback.
|
||||
var path = "/login";
|
||||
var data = {
|
||||
"type": "m.login.password",
|
||||
"user": userId,
|
||||
"password": password
|
||||
};
|
||||
return doRequest("POST", path, undefined, data);
|
||||
},
|
||||
|
||||
// hit the Identity Server for a 3PID request.
|
||||
linkEmail: function(email, clientSecret, sendAttempt) {
|
||||
var path = "/_matrix/identity/api/v1/validate/email/requestToken";
|
||||
var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
|
||||
var headers = {};
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||
},
|
||||
|
||||
authEmail: function(clientSecret, sid, code) {
|
||||
var path = "/_matrix/identity/api/v1/validate/email/submitToken";
|
||||
var data = "token="+code+"&sid="+sid+"&clientSecret="+clientSecret;
|
||||
var headers = {};
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||
},
|
||||
|
||||
bindEmail: function(userId, tokenId, clientSecret) {
|
||||
var path = "/_matrix/identity/api/v1/3pid/bind";
|
||||
var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
||||
var headers = {};
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||
},
|
||||
|
||||
lookup3pid: function(medium, address) {
|
||||
var path = "/_matrix/identity/api/v1/lookup?medium="+encodeURIComponent(medium)+"&address="+encodeURIComponent(address);
|
||||
return doBaseRequest(config.identityServer, "GET", path, {}, undefined, {});
|
||||
},
|
||||
|
||||
uploadContent: function(file) {
|
||||
var path = "/_matrix/content";
|
||||
var headers = {
|
||||
"Content-Type": undefined // undefined means angular will figure it out
|
||||
};
|
||||
var params = {
|
||||
access_token: config.access_token
|
||||
};
|
||||
|
||||
// If the file is actually a Blob object, prevent $http from JSON-stringified it before sending
|
||||
// (Equivalent to jQuery ajax processData = false)
|
||||
var $httpParams;
|
||||
if (file instanceof Blob) {
|
||||
$httpParams = {
|
||||
transformRequest: angular.identity
|
||||
};
|
||||
}
|
||||
|
||||
return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start listening on /events
|
||||
* @param {String} from the token from which to listen events to
|
||||
* @param {Integer} serverTimeout the time in ms the server will hold open the connection
|
||||
* @param {Integer} clientTimeout the timeout in ms used at the client HTTP request level
|
||||
* @returns a promise
|
||||
*/
|
||||
getEventStream: function(from, serverTimeout, clientTimeout) {
|
||||
var path = "/events";
|
||||
var params = {
|
||||
from: from,
|
||||
timeout: serverTimeout
|
||||
};
|
||||
|
||||
var $httpParams;
|
||||
if (clientTimeout) {
|
||||
// If the Internet connection is lost, this timeout is used to be able to
|
||||
// cancel the current request and notify the client so that it can retry with a new request.
|
||||
$httpParams = {
|
||||
timeout: clientTimeout
|
||||
};
|
||||
}
|
||||
|
||||
return doRequest("GET", path, params, undefined, $httpParams);
|
||||
},
|
||||
|
||||
// Indicates if user authentications details are stored in cache
|
||||
isUserLoggedIn: function() {
|
||||
var config = this.config();
|
||||
|
||||
// User is considered logged in if his cache is not empty and contains
|
||||
// an access token
|
||||
if (config && config.access_token) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Enum of presence state
|
||||
presence: {
|
||||
offline: "offline",
|
||||
unavailable: "unavailable",
|
||||
online: "online",
|
||||
free_for_chat: "free_for_chat"
|
||||
},
|
||||
|
||||
// Set the logged in user presence state
|
||||
setUserPresence: function(presence) {
|
||||
var path = "/presence/$user_id/status";
|
||||
path = path.replace("$user_id", encodeURIComponent(config.user_id));
|
||||
return doRequest("PUT", path, undefined, {
|
||||
presence: presence
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/****** Permanent storage of user information ******/
|
||||
|
||||
// Returns the current config
|
||||
config: function() {
|
||||
if (!config) {
|
||||
config = localStorage.getItem("config");
|
||||
if (config) {
|
||||
config = JSON.parse(config);
|
||||
|
||||
// Reset the cache if the version loaded is not the expected one
|
||||
if (configVersion !== config.version) {
|
||||
config = undefined;
|
||||
this.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
// Set a new config (Use saveConfig to actually store it permanently)
|
||||
setConfig: function(newConfig) {
|
||||
config = newConfig;
|
||||
console.log("new IS: "+config.identityServer);
|
||||
},
|
||||
|
||||
// Commits config into permanent storage
|
||||
saveConfig: function() {
|
||||
config.version = configVersion;
|
||||
localStorage.setItem("config", JSON.stringify(config));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 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, 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));
|
||||
|
||||
return doRequest("PUT", path, undefined, content);
|
||||
},
|
||||
|
||||
getTurnServer: function() {
|
||||
return doRequest("GET", "/voip/turnServer");
|
||||
}
|
||||
|
||||
};
|
||||
}]);
|
@ -1,349 +0,0 @@
|
||||
/*
|
||||
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, aliasToRoomId;
|
||||
var setRoomIdToAliasMapping = function(roomId, alias) {
|
||||
roomIdToAlias[roomId] = alias;
|
||||
aliasToRoomId[alias] = roomId;
|
||||
};
|
||||
|
||||
// user > room member lookups
|
||||
var userIdToRoomMember;
|
||||
|
||||
// main store
|
||||
var rooms, users;
|
||||
|
||||
var init = function() {
|
||||
roomIdToAlias = {};
|
||||
aliasToRoomId = {};
|
||||
userIdToRoomMember = {
|
||||
// user_id: [RoomMember, RoomMember, ...]
|
||||
};
|
||||
|
||||
// rooms are stored here when they come in.
|
||||
rooms = {
|
||||
// roomid: <Room>
|
||||
};
|
||||
|
||||
users = {
|
||||
// user_id: <User>
|
||||
};
|
||||
console.log("Models inited.");
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
/***** 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.now = this.current_room_state; // makes html access shorter
|
||||
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) {
|
||||
var keyIndex = event.state_key === undefined ? event.type : event.type + event.state_key;
|
||||
this.state_events[keyIndex] = event;
|
||||
if (event.type === "m.room.member") {
|
||||
var userId = event.state_key;
|
||||
var rm = new RoomMember();
|
||||
rm.event = event;
|
||||
rm.user = users[userId];
|
||||
this.members[userId] = rm;
|
||||
|
||||
// add to lookup so new m.presence events update the user
|
||||
if (!userIdToRoomMember[userId]) {
|
||||
userIdToRoomMember[userId] = [];
|
||||
}
|
||||
userIdToRoomMember[userId].push(rm);
|
||||
}
|
||||
else if (event.type === "m.room.aliases") {
|
||||
setRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
|
||||
}
|
||||
else if (event.type === "m.room.power_levels") {
|
||||
// normalise power levels: find the max first.
|
||||
var maxPowerLevel = 0;
|
||||
for (var user_id in event.content) {
|
||||
if (!event.content.hasOwnProperty(user_id) || user_id === "hsob_ts") continue; // XXX hsob_ts on some old rooms :(
|
||||
maxPowerLevel = Math.max(maxPowerLevel, event.content[user_id]);
|
||||
}
|
||||
// set power level f.e room member
|
||||
var defaultPowerLevel = event.content.default === undefined ? 0 : event.content.default;
|
||||
for (var user_id in this.members) {
|
||||
if (!this.members.hasOwnProperty(user_id)) continue;
|
||||
var rm = this.members[user_id];
|
||||
if (!rm) {
|
||||
continue;
|
||||
}
|
||||
rm.power_level = event.content[user_id] === undefined ? defaultPowerLevel : event.content[user_id];
|
||||
rm.power_level_norm = (rm.power_level * 100) / maxPowerLevel;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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.power_level_norm = 0;
|
||||
this.power_level = 0;
|
||||
this.user = undefined; // the User
|
||||
};
|
||||
|
||||
/***** User Object *****/
|
||||
var User = function User() {
|
||||
this.event = {}; // the m.presence event representing the User.
|
||||
this.last_updated = 0; // used with last_active_ago to work out last seen times
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// migrate old data but clobber matching keys
|
||||
if (users[event.content.user_id] && users[event.content.user_id].event) {
|
||||
angular.extend(users[event.content.user_id].event, event);
|
||||
usr = users[event.content.user_id];
|
||||
}
|
||||
else {
|
||||
users[event.content.user_id] = usr;
|
||||
}
|
||||
|
||||
usr.last_updated = new Date().getTime();
|
||||
|
||||
// update room members
|
||||
var roomMembers = userIdToRoomMember[event.content.user_id];
|
||||
if (roomMembers) {
|
||||
for (var i=0; i<roomMembers.length; i++) {
|
||||
var rm = roomMembers[i];
|
||||
rm.user = usr;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
getUserPowerLevel: function(room_id, user_id) {
|
||||
var powerLevel = 0;
|
||||
var room = this.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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
getUserCountInRoom: function(room_id) {
|
||||
var memberCount;
|
||||
|
||||
var room = this.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 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 = this.getRoom(room_id).events;
|
||||
for (var i = events.length - 1; i >= 0; i--) {
|
||||
var message = events[i];
|
||||
|
||||
// TODO: define a better marker than echo_msg_state
|
||||
if (!filterEcho || undefined === message.echo_msg_state) {
|
||||
lastMessage = message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lastMessage;
|
||||
},
|
||||
|
||||
clearRooms: function() {
|
||||
init();
|
||||
}
|
||||
|
||||
};
|
||||
}]);
|
@ -1,104 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
@ -1,113 +0,0 @@
|
||||
/*
|
||||
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 tracks user activity on the page to determine his presence state.
|
||||
* Any state change will be sent to the Home Server.
|
||||
*/
|
||||
angular.module('mPresence', [])
|
||||
.service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) {
|
||||
|
||||
// Time in ms after that a user is considered as unavailable/away
|
||||
var UNAVAILABLE_TIME = 3 * 60000; // 3 mins
|
||||
|
||||
// The current presence state
|
||||
var state = undefined;
|
||||
|
||||
var self =this;
|
||||
var timer;
|
||||
|
||||
/**
|
||||
* Start listening the user activity to evaluate his presence state.
|
||||
* Any state change will be sent to the Home Server.
|
||||
*/
|
||||
this.start = function() {
|
||||
if (undefined === state) {
|
||||
// The user is online if he moves the mouser or press a key
|
||||
document.onmousemove = resetTimer;
|
||||
document.onkeypress = resetTimer;
|
||||
|
||||
resetTimer();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop tracking user activity
|
||||
*/
|
||||
this.stop = function() {
|
||||
if (timer) {
|
||||
$timeout.cancel(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
state = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current presence state.
|
||||
* @returns {matrixService.presence} the presence state
|
||||
*/
|
||||
this.getState = function() {
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the presence state.
|
||||
* If the state has changed, the Home Server will be notified.
|
||||
* @param {matrixService.presence} newState the new presence state
|
||||
*/
|
||||
this.setState = function(newState) {
|
||||
if (newState !== state) {
|
||||
console.log("mPresence - New state: " + newState);
|
||||
|
||||
state = newState;
|
||||
|
||||
// Inform the HS on the new user state
|
||||
matrixService.setUserPresence(state).then(
|
||||
function() {
|
||||
|
||||
},
|
||||
function(error) {
|
||||
console.log("mPresence - Failed to send new presence state: " + JSON.stringify(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
|
||||
* @private
|
||||
*/
|
||||
function onUnvailableTimerFire() {
|
||||
self.setState(matrixService.presence.unavailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the user made an action on the page
|
||||
* @private
|
||||
*/
|
||||
function resetTimer() {
|
||||
// User is still here
|
||||
self.setState(matrixService.presence.online);
|
||||
|
||||
// Re-arm the timer
|
||||
$timeout.cancel(timer);
|
||||
timer = $timeout(onUnvailableTimerFire, UNAVAILABLE_TIME);
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
|
@ -1,99 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@ -1,151 +0,0 @@
|
||||
/*
|
||||
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 multipurpose helper functions.
|
||||
*/
|
||||
angular.module('mUtilities', [])
|
||||
.service('mUtilities', ['$q', function ($q) {
|
||||
/*
|
||||
* Get the size of an image
|
||||
* @param {File|Blob} imageFile the file containing the image
|
||||
* @returns {promise} A promise that will be resolved by an object with 2 members:
|
||||
* width & height
|
||||
*/
|
||||
this.getImageSize = function(imageFile) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
// Load the file into an html element
|
||||
var img = document.createElement("img");
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
img.src = e.target.result;
|
||||
|
||||
// Once ready, returns its size
|
||||
img.onload = function() {
|
||||
deferred.resolve({
|
||||
width: img.width,
|
||||
height: img.height
|
||||
});
|
||||
};
|
||||
img.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
reader.readAsDataURL(imageFile);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/*
|
||||
* Resize the image to fit in a square of the side maxSize.
|
||||
* The aspect ratio is kept. The returned image data uses JPEG compression.
|
||||
* Source: http://hacks.mozilla.org/2011/01/how-to-develop-a-html5-image-uploader/
|
||||
* @param {File} imageFile the file containing the image
|
||||
* @param {Integer} maxSize the max side size
|
||||
* @returns {promise} A promise that will be resolved by a Blob object containing
|
||||
* the resized image data
|
||||
*/
|
||||
this.resizeImage = function(imageFile, maxSize) {
|
||||
var self = this;
|
||||
var deferred = $q.defer();
|
||||
|
||||
var canvas = document.createElement("canvas");
|
||||
|
||||
var img = document.createElement("img");
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
|
||||
img.src = e.target.result;
|
||||
|
||||
// Once ready, returns its size
|
||||
img.onload = function() {
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
var MAX_WIDTH = maxSize;
|
||||
var MAX_HEIGHT = maxSize;
|
||||
var width = img.width;
|
||||
var height = img.height;
|
||||
|
||||
if (width > height) {
|
||||
if (width > MAX_WIDTH) {
|
||||
height *= MAX_WIDTH / width;
|
||||
width = MAX_WIDTH;
|
||||
}
|
||||
} else {
|
||||
if (height > MAX_HEIGHT) {
|
||||
width *= MAX_HEIGHT / height;
|
||||
height = MAX_HEIGHT;
|
||||
}
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Extract image data in the same format as the original one.
|
||||
// The 0.7 compression value will work with formats that supports it like JPEG.
|
||||
var dataUrl = canvas.toDataURL(imageFile.type, 0.7);
|
||||
deferred.resolve(self.dataURItoBlob(dataUrl));
|
||||
};
|
||||
img.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
reader.readAsDataURL(imageFile);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert a dataURI string to a blob
|
||||
* Source: http://stackoverflow.com/a/17682951
|
||||
* @param {String} dataURI the dataURI can be a base64 encoded string or an URL encoded string.
|
||||
* @returns {Blob} the blob
|
||||
*/
|
||||
this.dataURItoBlob = function(dataURI) {
|
||||
// convert base64 to raw binary data held in a string
|
||||
// doesn't handle URLEncoded DataURIs
|
||||
var byteString;
|
||||
if (dataURI.split(',')[0].indexOf('base64') >= 0)
|
||||
byteString = atob(dataURI.split(',')[1]);
|
||||
else
|
||||
byteString = unescape(dataURI.split(',')[1]);
|
||||
// separate out the mime component
|
||||
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
||||
|
||||
// write the bytes of the string to an ArrayBuffer
|
||||
var ab = new ArrayBuffer(byteString.length);
|
||||
var ia = new Uint8Array(ab);
|
||||
for (var i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// write the ArrayBuffer to a blob, and you're done
|
||||
return new Blob([ab],{type: mimeString});
|
||||
};
|
||||
|
||||
}]);
|
Before Width: | Height: | Size: 198 B |
@ -1,209 +0,0 @@
|
||||
/*
|
||||
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('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
|
||||
.controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService', 'modelService', 'recentsService',
|
||||
function($scope, $location, matrixService, eventHandlerService, modelService, recentsService) {
|
||||
|
||||
$scope.config = matrixService.config();
|
||||
$scope.public_rooms = [];
|
||||
$scope.newRoomId = "";
|
||||
$scope.feedback = "";
|
||||
|
||||
$scope.newRoom = {
|
||||
room_id: "",
|
||||
private: false
|
||||
};
|
||||
|
||||
$scope.goToRoom = {
|
||||
room_id: ""
|
||||
};
|
||||
|
||||
$scope.joinAlias = {
|
||||
room_alias: ""
|
||||
};
|
||||
|
||||
$scope.profile = {
|
||||
displayName: "",
|
||||
avatarUrl: ""
|
||||
};
|
||||
|
||||
$scope.newChat = {
|
||||
user: ""
|
||||
};
|
||||
|
||||
recentsService.setSelectedRoomId(undefined);
|
||||
|
||||
var refresh = function() {
|
||||
|
||||
matrixService.publicRooms().then(
|
||||
function(response) {
|
||||
$scope.public_rooms = response.data.chunk;
|
||||
for (var i = 0; i < $scope.public_rooms.length; i++) {
|
||||
var room = $scope.public_rooms[i];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.createNewRoom = function(room_alias, isPrivate) {
|
||||
|
||||
var visibility = "public";
|
||||
if (isPrivate) {
|
||||
visibility = "private";
|
||||
}
|
||||
|
||||
matrixService.create(room_alias, visibility).then(
|
||||
function(response) {
|
||||
// This room has been created. Refresh the rooms list
|
||||
console.log("Created room " + response.data.room_alias + " with id: "+
|
||||
response.data.room_id);
|
||||
modelService.createRoomIdToAliasMapping(
|
||||
response.data.room_id, response.data.room_alias);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + JSON.stringify(error.data);
|
||||
});
|
||||
};
|
||||
|
||||
// Go to a room
|
||||
$scope.goToRoom = function(room_id) {
|
||||
matrixService.join(room_id).then(
|
||||
function(response) {
|
||||
var final_room_id = room_id;
|
||||
if (response.data.hasOwnProperty("room_id")) {
|
||||
final_room_id = response.data.room_id;
|
||||
}
|
||||
|
||||
// TODO: factor out the common housekeeping whenever we try to join a room or alias
|
||||
matrixService.roomState(final_room_id).then(
|
||||
function(response) {
|
||||
eventHandlerService.handleEvents(response.data, false, true);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failed to get room state for: " + final_room_id;
|
||||
}
|
||||
);
|
||||
|
||||
$location.url("room/" + final_room_id);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + JSON.stringify(error.data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.joinAlias = function(room_alias) {
|
||||
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;
|
||||
}
|
||||
);
|
||||
// Go to this room
|
||||
$location.url("room/" + room_alias);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + JSON.stringify(error.data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// FIXME: factor this out between user-controller and home-controller etc.
|
||||
$scope.messageUser = function() {
|
||||
|
||||
// FIXME: create a new room every time, for now
|
||||
|
||||
matrixService.create(null, 'private').then(
|
||||
function(response) {
|
||||
// This room has been created. Refresh the rooms list
|
||||
var room_id = response.data.room_id;
|
||||
console.log("Created room with id: "+ room_id);
|
||||
|
||||
matrixService.invite(room_id, $scope.newChat.user).then(
|
||||
function() {
|
||||
$scope.feedback = "Invite sent successfully";
|
||||
$scope.$parent.goToPage("/room/" + room_id);
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failure: " + JSON.stringify(reason);
|
||||
});
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + JSON.stringify(error.data);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
$scope.onInit = function() {
|
||||
// Load profile data
|
||||
// Display name
|
||||
matrixService.getDisplayName($scope.config.user_id).then(
|
||||
function(response) {
|
||||
$scope.profile.displayName = response.data.displayname;
|
||||
var config = matrixService.config();
|
||||
config.display_name = response.data.displayname;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't load display name";
|
||||
}
|
||||
);
|
||||
// Avatar
|
||||
matrixService.getProfilePictureUrl($scope.config.user_id).then(
|
||||
function(response) {
|
||||
$scope.profile.avatarUrl = response.data.avatar_url;
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't load avatar URL";
|
||||
}
|
||||
);
|
||||
|
||||
// Listen to room creation event in order to update the public rooms list
|
||||
$scope.$on(eventHandlerService.ROOM_CREATE_EVENT, function(ngEvent, event, isLive) {
|
||||
if (isLive) {
|
||||
// As we do not know if this room is public, do a full list refresh
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Clean data when user logs out
|
||||
$scope.$on(eventHandlerService.RESET_EVENT, function() {
|
||||
$scope.public_rooms = [];
|
||||
});
|
||||
}]);
|
@ -1,78 +0,0 @@
|
||||
<div ng-controller="HomeController" data-ng-init="onInit()">
|
||||
|
||||
<div id="wrapper">
|
||||
|
||||
<div id="genericHeading">
|
||||
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
|
||||
</div>
|
||||
|
||||
<h1>Welcome to homeserver {{ config.homeserver }}</h1>
|
||||
|
||||
<div>
|
||||
<div class="profile-avatar">
|
||||
<img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}"/>
|
||||
</div>
|
||||
<div id="user-ids">
|
||||
<div id="user-displayname">{{ profile.displayName }}</div>
|
||||
<div>{{ config.user_id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Recent conversations</h3>
|
||||
<div ng-include="'recents/recents.html'"></div>
|
||||
<br/>
|
||||
|
||||
<h3>Public rooms</h3>
|
||||
|
||||
<table class="publicTable">
|
||||
<tbody ng-repeat="room in public_rooms | orderBy:'room_display_name'"
|
||||
class="publicRoomEntry"
|
||||
ng-class="room.room_display_name.toLowerCase().indexOf('#matrix:') === 0 ? 'roomHighlight' : ''">
|
||||
<tr>
|
||||
<td class="publicRoomEntry">
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >
|
||||
{{ room.room_display_name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="publicRoomJoinedUsers"
|
||||
ng-show="room.num_joined_members">
|
||||
{{ room.num_joined_members }} {{ room.num_joined_members == 1 ? 'user' : 'users' }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="publicRoomTopic">
|
||||
{{ room.topic }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newRoom.room_alias" ng-enter="createNewRoom(newRoom.room_alias, newRoom.private)" placeholder="(e.g. foo)"/>
|
||||
<input type="checkbox" ng-model="newRoom.private">private
|
||||
<button ng-disabled="!newRoom.room_alias" ng-click="createNewRoom(newRoom.room_alias, newRoom.private)">Create room</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo:example.org)"/>
|
||||
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newChat.user" ng-enter="messageUser()" placeholder="e.g. @user:domain.com"/>
|
||||
<button ng-disabled="!newChat.user" ng-click="messageUser()">Message user</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|
Before Width: | Height: | Size: 473 B |
Before Width: | Height: | Size: 397 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 194 B |
Before Width: | Height: | Size: 434 B |
Before Width: | Height: | Size: 910 B |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 864 B |
Before Width: | Height: | Size: 604 B |
Before Width: | Height: | Size: 659 B |
@ -1,139 +0,0 @@
|
||||
<!doctype html>
|
||||
<html xmlns:ng="http://angularjs.org" ng-app="matrixWebClient" ng-controller="MatrixWebClientController">
|
||||
<head>
|
||||
<title>[matrix]</title>
|
||||
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<link rel="stylesheet" href="mobile.css">
|
||||
<link rel="stylesheet" href="bootstrap.css">
|
||||
|
||||
<link rel="icon" href="favicon.ico">
|
||||
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<script type="text/javascript" src="js/jquery-1.8.3.min.js"></script>
|
||||
<script type="text/javascript" src="js/recaptcha_ajax.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/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>
|
||||
<script src="app-directive.js"></script>
|
||||
<script src="app-filter.js"></script>
|
||||
<script src="home/home-controller.js"></script>
|
||||
<script src="login/login-controller.js"></script>
|
||||
<script src="login/register-controller.js"></script>
|
||||
<script src="recents/recents-controller.js"></script>
|
||||
<script src="recents/recents-filter.js"></script>
|
||||
<script src="room/room-controller.js"></script>
|
||||
<script src="room/room-directive.js"></script>
|
||||
<script src="settings/settings-controller.js"></script>
|
||||
<script src="user/user-controller.js"></script>
|
||||
<script src="components/matrix/matrix-service.js"></script>
|
||||
<script src="components/matrix/matrix-filter.js"></script>
|
||||
<script src="components/matrix/matrix-call.js"></script>
|
||||
<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>
|
||||
<script src="components/utilities/utilities-service.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="videoBackground" ng-class="videoMode">
|
||||
<div id="videoContainer" ng-class="videoMode">
|
||||
<div id="videoContainerPadding"></div>
|
||||
<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>
|
||||
|
||||
<div id="header">
|
||||
<!-- Do not show buttons on the login page -->
|
||||
<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" ng-class="currentCall.state" />
|
||||
<div id="callPeerNameAndState">
|
||||
<span id="callPeerName">{{ currentCall.userProfile.displayname }}</span>
|
||||
<br />
|
||||
<span id="callState">
|
||||
<span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
|
||||
<span ng-show="currentCall.state == 'ringing' && currentCall && currentCall.type == 'video'">Incoming Video Call</span>
|
||||
<span ng-show="currentCall.state == 'ringing' && currentCall && currentCall.type == 'voice'">Incoming Voice Call</span>
|
||||
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
|
||||
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
|
||||
<span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'ice_failed'">Media Connection Failed{{ haveTurn ? "" : " (VoIP relaying unsupported by Home Server)" }}</span>
|
||||
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'remote'">Call Rejected</span>
|
||||
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">Call Canceled</span>
|
||||
<span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'invite_timeout' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">User Not Responding</span>
|
||||
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && currentCall.didConnect && currentCall.direction == 'outbound'">Call Ended</span>
|
||||
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'inbound'">Call Canceled</span>
|
||||
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && currentCall.didConnect && currentCall.direction == 'inbound'">Call Ended</span>
|
||||
<span ng-show="currentCall.state == 'wait_local_media'">Waiting for media permission...</span>
|
||||
</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="hangupCall()">Reject</button>
|
||||
</span>
|
||||
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
|
||||
<audio id="ringAudio" loop>
|
||||
<source src="media/ring.ogg" type="audio/ogg" />
|
||||
<source src="media/ring.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="ringbackAudio" loop>
|
||||
<source src="media/ringback.ogg" type="audio/ogg" />
|
||||
<source src="media/ringback.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="callendAudio">
|
||||
<source src="media/callend.ogg" type="audio/ogg" />
|
||||
<source src="media/callend.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="busyAudio">
|
||||
<source src="media/busy.ogg" type="audio/ogg" />
|
||||
<source src="media/busy.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a>
|
||||
|
||||
<button ng-click='goToPage("/")'>Home</button>
|
||||
<button ng-click='goToPage("settings")'>Settings</button>
|
||||
<button ng-click="logout()">Log out</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page" ng-hide="unsupportedBrowser" ng-view></div>
|
||||
|
||||
<div class="page" ng-show="unsupportedBrowser">
|
||||
<div id="unsupportedBrowser" ng-show="unsupportedBrowser">
|
||||
Sorry, your browser is not supported. <br/>
|
||||
Reason: {{ unsupportedBrowser.reason }}
|
||||
|
||||
<br/><br/>
|
||||
Your browser: <br/>
|
||||
{{ unsupportedBrowser.browser }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer" ng-hide="location.indexOf('/room') === 0">
|
||||
<div id="footerContent">
|
||||
© 2014 Matrix.org
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1880
syweb/webclient/js/angular-animate.js
vendored
30
syweb/webclient/js/angular-animate.min.js
vendored
@ -1,30 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.3.0-rc.1
|
||||
(c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(D,f,E){'use strict';f.module("ngAnimate",["ng"]).directive("ngAnimateChildren",function(){return function(P,w,g){g=g.ngAnimateChildren;f.isString(g)&&0===g.length?w.data("$$ngAnimateChildren",!0):P.$watch(g,function(f){w.data("$$ngAnimateChildren",!!f)})}}).factory("$$animateReflow",["$$rAF","$document",function(f,w){return function(g){return f(function(){g()})}}]).config(["$provide","$animateProvider",function(P,w){function g(f){for(var g=0;g<f.length;g++){var h=f[g];if(1==h.nodeType)return h}}
|
||||
function Z(f,h){return g(f)==g(h)}var q=f.noop,h=f.forEach,$=w.$$selectors,W=f.isArray,u={running:!0};P.decorator("$animate",["$delegate","$$q","$injector","$sniffer","$rootElement","$$asyncCallback","$rootScope","$document","$templateRequest",function(Q,D,I,E,v,X,x,Y,J){function K(a,c){var d=a.data("$$ngAnimateState")||{};c&&(d.running=!0,d.structural=!0,a.data("$$ngAnimateState",d));return d.disabled||d.running&&d.structural}function L(a){var c,d=D.defer();d.promise.$$cancelFn=function(){c&&c()};
|
||||
x.$$postDigest(function(){c=a(function(){d.resolve()})});return d.promise}function M(a,c,d){d=d||{};var e={};h(c.add,function(b){b&&b.length&&(e[b]=e[b]||0,e[b]++)});h(c.remove,function(b){b&&b.length&&(e[b]=e[b]||0,e[b]--)});var R=[];h(d,function(b,a){h(a.split(" "),function(a){R[a]=b})});var g=[],l=[];h(e,function(b,c){var d=f.$$hasClass(a[0],c),e=R[c]||{};0>b?(d||"addClass"==e.event)&&l.push(c):0<b&&(d&&"removeClass"!=e.event||g.push(c))});return 0<g.length+l.length&&[g.join(" "),l.join(" ")]}
|
||||
function N(a){if(a){var c=[],d={};a=a.substr(1).split(".");(E.transitions||E.animations)&&c.push(I.get($[""]));for(var e=0;e<a.length;e++){var f=a[e],g=$[f];g&&!d[f]&&(c.push(I.get(g)),d[f]=!0)}return c}}function T(a,c,d){function e(b,a){var c=b[a],d=b["before"+a.charAt(0).toUpperCase()+a.substr(1)];if(c||d)return"leave"==a&&(d=c,c=null),U.push({event:a,fn:c}),m.push({event:a,fn:d}),!0}function f(c,r,e){var A=[];h(c,function(b){b.fn&&A.push(b)});var k=0;h(A,function(c,f){var g=function(){a:{if(r){(r[f]||
|
||||
q)();if(++k<A.length)break a;r=null}e()}};switch(c.event){case "setClass":r.push(c.fn(a,l,b,g));break;case "addClass":r.push(c.fn(a,l||d,g));break;case "removeClass":r.push(c.fn(a,b||d,g));break;default:r.push(c.fn(a,g))}});r&&0===r.length&&e()}var g=a[0];if(g){var l,b;W(d)&&(l=d[0],b=d[1],l?b?d=l+" "+b:(d=l,c="addClass"):(d=b,c="removeClass"));var r="setClass"==c,A=r||"addClass"==c||"removeClass"==c,y=a.attr("class")+" "+d;if(B(y)){var C=q,k=[],m=[],n=q,s=[],U=[],y=(" "+y).replace(/\s+/g,".");h(N(y),
|
||||
function(b){!e(b,c)&&r&&(e(b,"addClass"),e(b,"removeClass"))});return{node:g,event:c,className:d,isClassBased:A,isSetClassOperation:r,before:function(b){C=b;f(m,k,function(){C=q;b()})},after:function(b){n=b;f(U,s,function(){n=q;b()})},cancel:function(){k&&(h(k,function(b){(b||q)(!0)}),C(!0));s&&(h(s,function(b){(b||q)(!0)}),n(!0))}}}}}function F(a,c,d,e,g,S,l){function b(b){var l="$animate:"+b;m&&m[l]&&0<m[l].length&&X(function(){d.triggerHandler(l,{event:a,className:c})})}function r(){b("before")}
|
||||
function A(){b("after")}function y(){y.hasBeenRun||(y.hasBeenRun=!0,S())}function C(){if(!C.hasBeenRun){C.hasBeenRun=!0;var r=d.data("$$ngAnimateState");r&&(k&&k.isClassBased?z(d,c):(X(function(){var b=d.data("$$ngAnimateState")||{};p==b.index&&z(d,c,a)}),d.data("$$ngAnimateState",r)));b("close");l()}}var k=T(d,a,c);if(!k)return y(),r(),A(),C(),q;a=k.event;c=k.className;var m=f.element._data(k.node),m=m&&m.events;e||(e=g?g.parent():d.parent());if(H(d,e))return y(),r(),A(),C(),q;e=d.data("$$ngAnimateState")||
|
||||
{};var n=e.active||{},s=e.totalActive||0,U=e.last;g=!1;if(0<s){s=[];if(k.isClassBased)"setClass"==U.event?(s.push(U),z(d,c)):n[c]&&(t=n[c],t.event==a?g=!0:(s.push(t),z(d,c)));else if("leave"==a&&n["ng-leave"])g=!0;else{for(var t in n)s.push(n[t]);e={};z(d,!0)}0<s.length&&h(s,function(b){b.cancel()})}!k.isClassBased||k.isSetClassOperation||g||(g="addClass"==a==d.hasClass(c));if(g)return y(),r(),A(),b("close"),l(),q;n=e.active||{};s=e.totalActive||0;if("leave"==a)d.one("$destroy",function(b){b=f.element(this);
|
||||
var a=b.data("$$ngAnimateState");a&&(a=a.active["ng-leave"])&&(a.cancel(),z(b,"ng-leave"))});d.addClass("ng-animate");var p=G++;s++;n[c]=k;d.data("$$ngAnimateState",{last:k,active:n,index:p,totalActive:s});r();k.before(function(b){var l=d.data("$$ngAnimateState");b=b||!l||!l.active[c]||k.isClassBased&&l.active[c].event!=a;y();!0===b?C():(A(),k.after(C))});return k.cancel}function p(a){if(a=g(a))a=f.isFunction(a.getElementsByClassName)?a.getElementsByClassName("ng-animate"):a.querySelectorAll(".ng-animate"),
|
||||
h(a,function(a){a=f.element(a);(a=a.data("$$ngAnimateState"))&&a.active&&h(a.active,function(a){a.cancel()})})}function z(a,c){if(Z(a,v))u.disabled||(u.running=!1,u.structural=!1);else if(c){var d=a.data("$$ngAnimateState")||{},e=!0===c;!e&&d.active&&d.active[c]&&(d.totalActive--,delete d.active[c]);if(e||!d.totalActive)a.removeClass("ng-animate"),a.removeData("$$ngAnimateState")}}function H(a,c){if(u.disabled)return!0;if(Z(a,v))return u.running;var d,e,g;do{if(0===c.length)break;var h=Z(c,v),l=h?
|
||||
u:c.data("$$ngAnimateState")||{};if(l.disabled)return!0;h&&(g=!0);!1!==d&&(h=c.data("$$ngAnimateChildren"),f.isDefined(h)&&(d=h));e=e||l.running||l.last&&!l.last.isClassBased}while(c=c.parent());return!g||!d&&e}v.data("$$ngAnimateState",u);var V=x.$watch(function(){return J.totalPendingRequests},function(a,c){0===a&&(V(),x.$$postDigest(function(){x.$$postDigest(function(){u.running=!1})}))}),G=0,O=w.classNameFilter(),B=O?function(a){return O.test(a)}:function(){return!0};return{enter:function(a,c,
|
||||
d){a=f.element(a);c=c&&f.element(c);d=d&&f.element(d);K(a,!0);Q.enter(a,c,d);return L(function(e){return F("enter","ng-enter",f.element(g(a)),c,d,q,e)})},leave:function(a){a=f.element(a);p(a);K(a,!0);this.enabled(!1,a);return L(function(c){return F("leave","ng-leave",f.element(g(a)),null,null,function(){Q.leave(a)},c)})},move:function(a,c,d){a=f.element(a);c=c&&f.element(c);d=d&&f.element(d);p(a);K(a,!0);Q.move(a,c,d);return L(function(e){return F("move","ng-move",f.element(g(a)),c,d,q,e)})},addClass:function(a,
|
||||
c){return this.setClass(a,c,[])},removeClass:function(a,c){return this.setClass(a,[],c)},setClass:function(a,c,d){a=f.element(a);a=f.element(g(a));if(K(a))return Q.setClass(a,c,d);c=W(c)?c:c.split(" ");d=W(d)?d:d.split(" ");var e=a.data("$$animateClasses");if(e)return e.add=e.add.concat(c),e.remove=e.remove.concat(d),e.promise;a.data("$$animateClasses",e={add:c,remove:d});return e.promise=L(function(c){var d=a.data("$$animateClasses");a.removeData("$$animateClasses");var l=a.data("$$ngAnimateState")||
|
||||
{},b=M(a,d,l.active);return b?F("setClass",b,a,null,null,function(){Q.setClass(a,b[0],b[1])},c):c()})},cancel:function(a){a.$$cancelFn()},enabled:function(a,c){switch(arguments.length){case 2:if(a)z(c);else{var d=c.data("$$ngAnimateState")||{};d.disabled=!0;c.data("$$ngAnimateState",d)}break;case 1:u.disabled=!a;break;default:a=!u.disabled}return!!a}}}]);w.register("",["$window","$sniffer","$timeout","$$animateReflow",function(u,w,I,P){function v(a,b){d&&d();c.push(b);d=P(function(){h(c,function(b){b()});
|
||||
c=[];d=null;B={}})}function X(a,b){var c=g(a);a=f.element(c);S.push(a);c=Date.now()+b;c<=R||(I.cancel(e),R=c,e=I(function(){x(S);S=[]},b,!1))}function x(a){h(a,function(b){(b=b.data("$$ngAnimateCSS3Data"))&&h(b.closeAnimationFns,function(b){b()})})}function Y(a,b){var c=b?B[b]:null;if(!c){var d=0,e=0,g=0,f=0;h(a,function(b){if(1==b.nodeType){b=u.getComputedStyle(b)||{};d=Math.max(J(b[H+"Duration"]),d);e=Math.max(J(b[H+"Delay"]),e);f=Math.max(J(b[G+"Delay"]),f);var a=J(b[G+"Duration"]);0<a&&(a*=parseInt(b[G+
|
||||
"IterationCount"],10)||1);g=Math.max(a,g)}});c={total:0,transitionDelay:e,transitionDuration:d,animationDelay:f,animationDuration:g};b&&(B[b]=c)}return c}function J(a){var b=0;a=f.isString(a)?a.split(/\s*,\s*/):[];h(a,function(a){b=Math.max(parseFloat(a)||0,b)});return b}function K(c,b,d){c=0<=["ng-enter","ng-leave","ng-move"].indexOf(d);var e,f=b.parent(),h=f.data("$$ngAnimateKey");h||(f.data("$$ngAnimateKey",++a),h=a);e=h+"-"+g(b).getAttribute("class");var f=e+" "+d,h=B[f]?++B[f].total:0,k={};if(0<
|
||||
h){var m=d+"-stagger",k=e+" "+m;(e=!B[k])&&b.addClass(m);k=Y(b,k);e&&b.removeClass(m)}b.addClass(d);var m=b.data("$$ngAnimateCSS3Data")||{},n=Y(b,f);e=n.transitionDuration;n=n.animationDuration;if(c&&0===e&&0===n)return b.removeClass(d),!1;d=c&&0<e;c=0<n&&0<k.animationDelay&&0===k.animationDuration;b.data("$$ngAnimateCSS3Data",{stagger:k,cacheKey:f,running:m.running||0,itemIndex:h,blockTransition:d,closeAnimationFns:m.closeAnimationFns||[]});b=g(b);d&&(b.style[H+"Property"]="none");c&&(b.style[G+
|
||||
"PlayState"]="paused");return!0}function L(a,b,c,d){function e(a){b.off(D,f);b.removeClass(m);b.removeClass(n);x&&I.cancel(x);F(b,c);a=g(b);for(var d in s)a.style.removeProperty(s[d])}function f(b){b.stopPropagation();var a=b.originalEvent||b;b=a.$manualTimeStamp||a.timeStamp||Date.now();a=parseFloat(a.elapsedTime.toFixed(3));Math.max(b-E,0)>=B&&a>=w&&d()}var k=g(b);a=b.data("$$ngAnimateCSS3Data");if(-1!=k.getAttribute("class").indexOf(c)&&a){a.blockTransition&&(k.style[H+"Property"]="");var m="",
|
||||
n="";h(c.split(" "),function(b,a){var c=(0<a?" ":"")+b;m+=c+"-active";n+=c+"-pending"});var s=[],p=a.itemIndex,t=a.stagger,q=0;if(0<p){q=0;0<t.transitionDelay&&0===t.transitionDuration&&(q=t.transitionDelay*p);var u=0;0<t.animationDelay&&0===t.animationDuration&&(u=t.animationDelay*p,s.push(z+"animation-play-state"));q=Math.round(100*Math.max(q,u))/100}q||b.addClass(m);var v=Y(b,a.cacheKey+" "+m),w=Math.max(v.transitionDuration,v.animationDuration);if(0===w)b.removeClass(m),F(b,c),d();else{var p=
|
||||
Math.max(v.transitionDelay,v.animationDelay),B=1E3*p;0<s.length&&(t=k.getAttribute("style")||"",";"!==t.charAt(t.length-1)&&(t+=";"),k.setAttribute("style",t+" "));var E=Date.now(),D=O+" "+V,p=1E3*(q+1.5*(p+w)),x;0<q&&(b.addClass(n),x=I(function(){x=null;b.addClass(m);b.removeClass(n);0<v.animationDuration&&(k.style[G+"PlayState"]="")},1E3*q,!1));b.on(D,f);a.closeAnimationFns.push(function(){e();d()});a.running++;X(b,p);return e}}else d()}function M(a,b,c,d){if(K(a,b,c,d))return function(a){a&&F(b,
|
||||
c)}}function N(a,b,c,d){if(b.data("$$ngAnimateCSS3Data"))return L(a,b,c,d);F(b,c);d()}function T(a,b,c,d){var e=M(a,b,c);if(e){var f=e;v(b,function(){f=N(a,b,c,d)});return function(b){(f||q)(b)}}d()}function F(a,b){a.removeClass(b);var c=a.data("$$ngAnimateCSS3Data");c&&(c.running&&c.running--,c.running&&0!==c.running||a.removeData("$$ngAnimateCSS3Data"))}function p(a,b){var c="";a=W(a)?a:a.split(/\s+/);h(a,function(a,d){a&&0<a.length&&(c+=(0<d?" ":"")+a+b)});return c}var z="",H,V,G,O;D.ontransitionend===
|
||||
E&&D.onwebkittransitionend!==E?(z="-webkit-",H="WebkitTransition",V="webkitTransitionEnd transitionend"):(H="transition",V="transitionend");D.onanimationend===E&&D.onwebkitanimationend!==E?(z="-webkit-",G="WebkitAnimation",O="webkitAnimationEnd animationend"):(G="animation",O="animationend");var B={},a=0,c=[],d,e=null,R=0,S=[];return{enter:function(a,b){return T("enter",a,"ng-enter",b)},leave:function(a,b){return T("leave",a,"ng-leave",b)},move:function(a,b){return T("move",a,"ng-move",b)},beforeSetClass:function(a,
|
||||
b,c,d){b=p(c,"-remove")+" "+p(b,"-add");if(b=M("setClass",a,b))return v(a,d),b;d()},beforeAddClass:function(a,b,c){if(b=M("addClass",a,p(b,"-add")))return v(a,c),b;c()},beforeRemoveClass:function(a,b,c){if(b=M("removeClass",a,p(b,"-remove")))return v(a,c),b;c()},setClass:function(a,b,c,d){c=p(c,"-remove");b=p(b,"-add");return N("setClass",a,c+" "+b,d)},addClass:function(a,b,c){return N("addClass",a,p(b,"-add"),c)},removeClass:function(a,b,c){return N("removeClass",a,p(b,"-remove"),c)}}}])}])})(window,
|
||||
window.angular);
|
||||
//# sourceMappingURL=angular-animate.min.js.map
|
2259
syweb/webclient/js/angular-mocks.js
vendored
69
syweb/webclient/js/angular-peity.js
vendored
@ -1,69 +0,0 @@
|
||||
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" );
|
||||
|
||||
} );
|
956
syweb/webclient/js/angular-route.js
vendored
@ -1,956 +0,0 @@
|
||||
/**
|
||||
* @license AngularJS v1.3.0-rc.1
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window, angular, undefined) {'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
* @name ngRoute
|
||||
* @description
|
||||
*
|
||||
* # ngRoute
|
||||
*
|
||||
* The `ngRoute` module provides routing and deeplinking services and directives for angular apps.
|
||||
*
|
||||
* ## Example
|
||||
* See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
|
||||
*
|
||||
*
|
||||
* <div doc-module-components="ngRoute"></div>
|
||||
*/
|
||||
/* global -ngRouteModule */
|
||||
var ngRouteModule = angular.module('ngRoute', ['ng']).
|
||||
provider('$route', $RouteProvider),
|
||||
$routeMinErr = angular.$$minErr('ngRoute');
|
||||
|
||||
/**
|
||||
* @ngdoc provider
|
||||
* @name $routeProvider
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Used for configuring routes.
|
||||
*
|
||||
* ## Example
|
||||
* See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
|
||||
*
|
||||
* ## Dependencies
|
||||
* Requires the {@link ngRoute `ngRoute`} module to be installed.
|
||||
*/
|
||||
function $RouteProvider(){
|
||||
function inherit(parent, extra) {
|
||||
return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra);
|
||||
}
|
||||
|
||||
var routes = {};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $routeProvider#when
|
||||
*
|
||||
* @param {string} path Route path (matched against `$location.path`). If `$location.path`
|
||||
* contains redundant trailing slash or is missing one, the route will still match and the
|
||||
* `$location.path` will be updated to add or drop the trailing slash to exactly match the
|
||||
* route definition.
|
||||
*
|
||||
* * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up
|
||||
* to the next slash are matched and stored in `$routeParams` under the given `name`
|
||||
* when the route matches.
|
||||
* * `path` can contain named groups starting with a colon and ending with a star:
|
||||
* e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name`
|
||||
* when the route matches.
|
||||
* * `path` can contain optional named groups with a question mark: e.g.`:name?`.
|
||||
*
|
||||
* For example, routes like `/color/:color/largecode/:largecode*\/edit` will match
|
||||
* `/color/brown/largecode/code/with/slashes/edit` and extract:
|
||||
*
|
||||
* * `color: brown`
|
||||
* * `largecode: code/with/slashes`.
|
||||
*
|
||||
*
|
||||
* @param {Object} route Mapping information to be assigned to `$route.current` on route
|
||||
* match.
|
||||
*
|
||||
* Object properties:
|
||||
*
|
||||
* - `controller` – `{(string|function()=}` – Controller fn that should be associated with
|
||||
* newly created scope or the name of a {@link angular.Module#controller registered
|
||||
* controller} if passed as a string.
|
||||
* - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be
|
||||
* published to scope under the `controllerAs` name.
|
||||
* - `template` – `{string=|function()=}` – html template as a string or a function that
|
||||
* returns an html template as a string which should be used by {@link
|
||||
* ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
|
||||
* This property takes precedence over `templateUrl`.
|
||||
*
|
||||
* If `template` is a function, it will be called with the following parameters:
|
||||
*
|
||||
* - `{Array.<Object>}` - route parameters extracted from the current
|
||||
* `$location.path()` by applying the current route
|
||||
*
|
||||
* - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
|
||||
* template that should be used by {@link ngRoute.directive:ngView ngView}.
|
||||
*
|
||||
* If `templateUrl` is a function, it will be called with the following parameters:
|
||||
*
|
||||
* - `{Array.<Object>}` - route parameters extracted from the current
|
||||
* `$location.path()` by applying the current route
|
||||
*
|
||||
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
|
||||
* be injected into the controller. If any of these dependencies are promises, the router
|
||||
* will wait for them all to be resolved or one to be rejected before the controller is
|
||||
* instantiated.
|
||||
* If all the promises are resolved successfully, the values of the resolved promises are
|
||||
* injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is
|
||||
* fired. If any of the promises are rejected the
|
||||
* {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object
|
||||
* is:
|
||||
*
|
||||
* - `key` – `{string}`: a name of a dependency to be injected into the controller.
|
||||
* - `factory` - `{string|function}`: If `string` then it is an alias for a service.
|
||||
* Otherwise if function, then it is {@link auto.$injector#invoke injected}
|
||||
* and the return value is treated as the dependency. If the result is a promise, it is
|
||||
* resolved before its value is injected into the controller. Be aware that
|
||||
* `ngRoute.$routeParams` will still refer to the previous route within these resolve
|
||||
* functions. Use `$route.current.params` to access the new route parameters, instead.
|
||||
*
|
||||
* - `redirectTo` – {(string|function())=} – value to update
|
||||
* {@link ng.$location $location} path with and trigger route redirection.
|
||||
*
|
||||
* If `redirectTo` is a function, it will be called with the following parameters:
|
||||
*
|
||||
* - `{Object.<string>}` - route parameters extracted from the current
|
||||
* `$location.path()` by applying the current route templateUrl.
|
||||
* - `{string}` - current `$location.path()`
|
||||
* - `{Object}` - current `$location.search()`
|
||||
*
|
||||
* The custom `redirectTo` function is expected to return a string which will be used
|
||||
* to update `$location.path()` and `$location.search()`.
|
||||
*
|
||||
* - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()`
|
||||
* or `$location.hash()` changes.
|
||||
*
|
||||
* If the option is set to `false` and url in the browser changes, then
|
||||
* `$routeUpdate` event is broadcasted on the root scope.
|
||||
*
|
||||
* - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive
|
||||
*
|
||||
* If the option is set to `true`, then the particular route can be matched without being
|
||||
* case sensitive
|
||||
*
|
||||
* @returns {Object} self
|
||||
*
|
||||
* @description
|
||||
* Adds a new route definition to the `$route` service.
|
||||
*/
|
||||
this.when = function(path, route) {
|
||||
routes[path] = angular.extend(
|
||||
{reloadOnSearch: true},
|
||||
route,
|
||||
path && pathRegExp(path, route)
|
||||
);
|
||||
|
||||
// create redirection for trailing slashes
|
||||
if (path) {
|
||||
var redirectPath = (path[path.length-1] == '/')
|
||||
? path.substr(0, path.length-1)
|
||||
: path +'/';
|
||||
|
||||
routes[redirectPath] = angular.extend(
|
||||
{redirectTo: path},
|
||||
pathRegExp(redirectPath, route)
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param path {string} path
|
||||
* @param opts {Object} options
|
||||
* @return {?Object}
|
||||
*
|
||||
* @description
|
||||
* Normalizes the given path, returning a regular expression
|
||||
* and the original path.
|
||||
*
|
||||
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
|
||||
*/
|
||||
function pathRegExp(path, opts) {
|
||||
var insensitive = opts.caseInsensitiveMatch,
|
||||
ret = {
|
||||
originalPath: path,
|
||||
regexp: path
|
||||
},
|
||||
keys = ret.keys = [];
|
||||
|
||||
path = path
|
||||
.replace(/([().])/g, '\\$1')
|
||||
.replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){
|
||||
var optional = option === '?' ? option : null;
|
||||
var star = option === '*' ? option : null;
|
||||
keys.push({ name: key, optional: !!optional });
|
||||
slash = slash || '';
|
||||
return ''
|
||||
+ (optional ? '' : slash)
|
||||
+ '(?:'
|
||||
+ (optional ? slash : '')
|
||||
+ (star && '(.+?)' || '([^/]+)')
|
||||
+ (optional || '')
|
||||
+ ')'
|
||||
+ (optional || '');
|
||||
})
|
||||
.replace(/([\/$\*])/g, '\\$1');
|
||||
|
||||
ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $routeProvider#otherwise
|
||||
*
|
||||
* @description
|
||||
* Sets route definition that will be used on route change when no other route definition
|
||||
* is matched.
|
||||
*
|
||||
* @param {Object|string} params Mapping information to be assigned to `$route.current`.
|
||||
* If called with a string, the value maps to `redirectTo`.
|
||||
* @returns {Object} self
|
||||
*/
|
||||
this.otherwise = function(params) {
|
||||
if (typeof params === 'string') {
|
||||
params = {redirectTo: params};
|
||||
}
|
||||
this.when(null, params);
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
this.$get = ['$rootScope',
|
||||
'$location',
|
||||
'$routeParams',
|
||||
'$q',
|
||||
'$injector',
|
||||
'$templateRequest',
|
||||
'$sce',
|
||||
function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) {
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $route
|
||||
* @requires $location
|
||||
* @requires $routeParams
|
||||
*
|
||||
* @property {Object} current Reference to the current route definition.
|
||||
* The route definition contains:
|
||||
*
|
||||
* - `controller`: The controller constructor as define in route definition.
|
||||
* - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for
|
||||
* controller instantiation. The `locals` contain
|
||||
* the resolved values of the `resolve` map. Additionally the `locals` also contain:
|
||||
*
|
||||
* - `$scope` - The current route scope.
|
||||
* - `$template` - The current route template HTML.
|
||||
*
|
||||
* @property {Object} routes Object with all route configuration Objects as its properties.
|
||||
*
|
||||
* @description
|
||||
* `$route` is used for deep-linking URLs to controllers and views (HTML partials).
|
||||
* It watches `$location.url()` and tries to map the path to an existing route definition.
|
||||
*
|
||||
* Requires the {@link ngRoute `ngRoute`} module to be installed.
|
||||
*
|
||||
* You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API.
|
||||
*
|
||||
* The `$route` service is typically used in conjunction with the
|
||||
* {@link ngRoute.directive:ngView `ngView`} directive and the
|
||||
* {@link ngRoute.$routeParams `$routeParams`} service.
|
||||
*
|
||||
* @example
|
||||
* This example shows how changing the URL hash causes the `$route` to match a route against the
|
||||
* URL, and the `ngView` pulls in the partial.
|
||||
*
|
||||
* Note that this example is using {@link ng.directive:script inlined templates}
|
||||
* to get it working on jsfiddle as well.
|
||||
*
|
||||
* <example name="$route-service" module="ngRouteExample"
|
||||
* deps="angular-route.js" fixBase="true">
|
||||
* <file name="index.html">
|
||||
* <div ng-controller="MainController">
|
||||
* Choose:
|
||||
* <a href="Book/Moby">Moby</a> |
|
||||
* <a href="Book/Moby/ch/1">Moby: Ch1</a> |
|
||||
* <a href="Book/Gatsby">Gatsby</a> |
|
||||
* <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
|
||||
* <a href="Book/Scarlet">Scarlet Letter</a><br/>
|
||||
*
|
||||
* <div ng-view></div>
|
||||
*
|
||||
* <hr />
|
||||
*
|
||||
* <pre>$location.path() = {{$location.path()}}</pre>
|
||||
* <pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre>
|
||||
* <pre>$route.current.params = {{$route.current.params}}</pre>
|
||||
* <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
|
||||
* <pre>$routeParams = {{$routeParams}}</pre>
|
||||
* </div>
|
||||
* </file>
|
||||
*
|
||||
* <file name="book.html">
|
||||
* controller: {{name}}<br />
|
||||
* Book Id: {{params.bookId}}<br />
|
||||
* </file>
|
||||
*
|
||||
* <file name="chapter.html">
|
||||
* controller: {{name}}<br />
|
||||
* Book Id: {{params.bookId}}<br />
|
||||
* Chapter Id: {{params.chapterId}}
|
||||
* </file>
|
||||
*
|
||||
* <file name="script.js">
|
||||
* angular.module('ngRouteExample', ['ngRoute'])
|
||||
*
|
||||
* .controller('MainController', function($scope, $route, $routeParams, $location) {
|
||||
* $scope.$route = $route;
|
||||
* $scope.$location = $location;
|
||||
* $scope.$routeParams = $routeParams;
|
||||
* })
|
||||
*
|
||||
* .controller('BookController', function($scope, $routeParams) {
|
||||
* $scope.name = "BookController";
|
||||
* $scope.params = $routeParams;
|
||||
* })
|
||||
*
|
||||
* .controller('ChapterController', function($scope, $routeParams) {
|
||||
* $scope.name = "ChapterController";
|
||||
* $scope.params = $routeParams;
|
||||
* })
|
||||
*
|
||||
* .config(function($routeProvider, $locationProvider) {
|
||||
* $routeProvider
|
||||
* .when('/Book/:bookId', {
|
||||
* templateUrl: 'book.html',
|
||||
* controller: 'BookController',
|
||||
* resolve: {
|
||||
* // I will cause a 1 second delay
|
||||
* delay: function($q, $timeout) {
|
||||
* var delay = $q.defer();
|
||||
* $timeout(delay.resolve, 1000);
|
||||
* return delay.promise;
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* .when('/Book/:bookId/ch/:chapterId', {
|
||||
* templateUrl: 'chapter.html',
|
||||
* controller: 'ChapterController'
|
||||
* });
|
||||
*
|
||||
* // configure html5 to get links working on jsfiddle
|
||||
* $locationProvider.html5Mode(true);
|
||||
* });
|
||||
*
|
||||
* </file>
|
||||
*
|
||||
* <file name="protractor.js" type="protractor">
|
||||
* it('should load and compile correct template', function() {
|
||||
* element(by.linkText('Moby: Ch1')).click();
|
||||
* var content = element(by.css('[ng-view]')).getText();
|
||||
* expect(content).toMatch(/controller\: ChapterController/);
|
||||
* expect(content).toMatch(/Book Id\: Moby/);
|
||||
* expect(content).toMatch(/Chapter Id\: 1/);
|
||||
*
|
||||
* element(by.partialLinkText('Scarlet')).click();
|
||||
*
|
||||
* content = element(by.css('[ng-view]')).getText();
|
||||
* expect(content).toMatch(/controller\: BookController/);
|
||||
* expect(content).toMatch(/Book Id\: Scarlet/);
|
||||
* });
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc event
|
||||
* @name $route#$routeChangeStart
|
||||
* @eventType broadcast on root scope
|
||||
* @description
|
||||
* Broadcasted before a route change. At this point the route services starts
|
||||
* resolving all of the dependencies needed for the route change to occur.
|
||||
* Typically this involves fetching the view template as well as any dependencies
|
||||
* defined in `resolve` route property. Once all of the dependencies are resolved
|
||||
* `$routeChangeSuccess` is fired.
|
||||
*
|
||||
* @param {Object} angularEvent Synthetic event object.
|
||||
* @param {Route} next Future route information.
|
||||
* @param {Route} current Current route information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc event
|
||||
* @name $route#$routeChangeSuccess
|
||||
* @eventType broadcast on root scope
|
||||
* @description
|
||||
* Broadcasted after a route dependencies are resolved.
|
||||
* {@link ngRoute.directive:ngView ngView} listens for the directive
|
||||
* to instantiate the controller and render the view.
|
||||
*
|
||||
* @param {Object} angularEvent Synthetic event object.
|
||||
* @param {Route} current Current route information.
|
||||
* @param {Route|Undefined} previous Previous route information, or undefined if current is
|
||||
* first route entered.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc event
|
||||
* @name $route#$routeChangeError
|
||||
* @eventType broadcast on root scope
|
||||
* @description
|
||||
* Broadcasted if any of the resolve promises are rejected.
|
||||
*
|
||||
* @param {Object} angularEvent Synthetic event object
|
||||
* @param {Route} current Current route information.
|
||||
* @param {Route} previous Previous route information.
|
||||
* @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc event
|
||||
* @name $route#$routeUpdate
|
||||
* @eventType broadcast on root scope
|
||||
* @description
|
||||
*
|
||||
* The `reloadOnSearch` property has been set to false, and we are reusing the same
|
||||
* instance of the Controller.
|
||||
*/
|
||||
|
||||
var forceReload = false,
|
||||
$route = {
|
||||
routes: routes,
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $route#reload
|
||||
*
|
||||
* @description
|
||||
* Causes `$route` service to reload the current route even if
|
||||
* {@link ng.$location $location} hasn't changed.
|
||||
*
|
||||
* As a result of that, {@link ngRoute.directive:ngView ngView}
|
||||
* creates new scope, reinstantiates the controller.
|
||||
*/
|
||||
reload: function() {
|
||||
forceReload = true;
|
||||
$rootScope.$evalAsync(updateRoute);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $route#updateParams
|
||||
*
|
||||
* @description
|
||||
* Causes `$route` service to update the current URL, replacing
|
||||
* current route parameters with those specified in `newParams`.
|
||||
* Provided property names that match the route's path segment
|
||||
* definitions will be interpolated into the location's path, while
|
||||
* remaining properties will be treated as query params.
|
||||
*
|
||||
* @param {Object} newParams mapping of URL parameter names to values
|
||||
*/
|
||||
updateParams: function(newParams) {
|
||||
if (this.current && this.current.$$route) {
|
||||
var searchParams = {}, self=this;
|
||||
|
||||
angular.forEach(Object.keys(newParams), function(key) {
|
||||
if (!self.current.pathParams[key]) searchParams[key] = newParams[key];
|
||||
});
|
||||
|
||||
newParams = angular.extend({}, this.current.params, newParams);
|
||||
$location.path(interpolate(this.current.$$route.originalPath, newParams));
|
||||
$location.search(angular.extend({}, $location.search(), searchParams));
|
||||
}
|
||||
else {
|
||||
throw $routeMinErr('norout', 'Tried updating route when with no current route');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$rootScope.$on('$locationChangeSuccess', updateRoute);
|
||||
|
||||
return $route;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @param on {string} current url
|
||||
* @param route {Object} route regexp to match the url against
|
||||
* @return {?Object}
|
||||
*
|
||||
* @description
|
||||
* Check if the route matches the current url.
|
||||
*
|
||||
* Inspired by match in
|
||||
* visionmedia/express/lib/router/router.js.
|
||||
*/
|
||||
function switchRouteMatcher(on, route) {
|
||||
var keys = route.keys,
|
||||
params = {};
|
||||
|
||||
if (!route.regexp) return null;
|
||||
|
||||
var m = route.regexp.exec(on);
|
||||
if (!m) return null;
|
||||
|
||||
for (var i = 1, len = m.length; i < len; ++i) {
|
||||
var key = keys[i - 1];
|
||||
|
||||
var val = m[i];
|
||||
|
||||
if (key && val) {
|
||||
params[key.name] = val;
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
function updateRoute() {
|
||||
var next = parseRoute(),
|
||||
last = $route.current;
|
||||
|
||||
if (next && last && next.$$route === last.$$route
|
||||
&& angular.equals(next.pathParams, last.pathParams)
|
||||
&& !next.reloadOnSearch && !forceReload) {
|
||||
last.params = next.params;
|
||||
angular.copy(last.params, $routeParams);
|
||||
$rootScope.$broadcast('$routeUpdate', last);
|
||||
} else if (next || last) {
|
||||
forceReload = false;
|
||||
$rootScope.$broadcast('$routeChangeStart', next, last);
|
||||
$route.current = next;
|
||||
if (next) {
|
||||
if (next.redirectTo) {
|
||||
if (angular.isString(next.redirectTo)) {
|
||||
$location.path(interpolate(next.redirectTo, next.params)).search(next.params)
|
||||
.replace();
|
||||
} else {
|
||||
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
|
||||
.replace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$q.when(next).
|
||||
then(function() {
|
||||
if (next) {
|
||||
var locals = angular.extend({}, next.resolve),
|
||||
template, templateUrl;
|
||||
|
||||
angular.forEach(locals, function(value, key) {
|
||||
locals[key] = angular.isString(value) ?
|
||||
$injector.get(value) : $injector.invoke(value, null, null, key);
|
||||
});
|
||||
|
||||
if (angular.isDefined(template = next.template)) {
|
||||
if (angular.isFunction(template)) {
|
||||
template = template(next.params);
|
||||
}
|
||||
} else if (angular.isDefined(templateUrl = next.templateUrl)) {
|
||||
if (angular.isFunction(templateUrl)) {
|
||||
templateUrl = templateUrl(next.params);
|
||||
}
|
||||
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
|
||||
if (angular.isDefined(templateUrl)) {
|
||||
next.loadedTemplateUrl = templateUrl;
|
||||
template = $templateRequest(templateUrl);
|
||||
}
|
||||
}
|
||||
if (angular.isDefined(template)) {
|
||||
locals['$template'] = template;
|
||||
}
|
||||
return $q.all(locals);
|
||||
}
|
||||
}).
|
||||
// after route change
|
||||
then(function(locals) {
|
||||
if (next == $route.current) {
|
||||
if (next) {
|
||||
next.locals = locals;
|
||||
angular.copy(next.params, $routeParams);
|
||||
}
|
||||
$rootScope.$broadcast('$routeChangeSuccess', next, last);
|
||||
}
|
||||
}, function(error) {
|
||||
if (next == $route.current) {
|
||||
$rootScope.$broadcast('$routeChangeError', next, last, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns {Object} the current active route, by matching it against the URL
|
||||
*/
|
||||
function parseRoute() {
|
||||
// Match a route
|
||||
var params, match;
|
||||
angular.forEach(routes, function(route, path) {
|
||||
if (!match && (params = switchRouteMatcher($location.path(), route))) {
|
||||
match = inherit(route, {
|
||||
params: angular.extend({}, $location.search(), params),
|
||||
pathParams: params});
|
||||
match.$$route = route;
|
||||
}
|
||||
});
|
||||
// No route matched; fallback to "otherwise" route
|
||||
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} interpolation of the redirect path with the parameters
|
||||
*/
|
||||
function interpolate(string, params) {
|
||||
var result = [];
|
||||
angular.forEach((string||'').split(':'), function(segment, i) {
|
||||
if (i === 0) {
|
||||
result.push(segment);
|
||||
} else {
|
||||
var segmentMatch = segment.match(/(\w+)(.*)/);
|
||||
var key = segmentMatch[1];
|
||||
result.push(params[key]);
|
||||
result.push(segmentMatch[2] || '');
|
||||
delete params[key];
|
||||
}
|
||||
});
|
||||
return result.join('');
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
ngRouteModule.provider('$routeParams', $RouteParamsProvider);
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $routeParams
|
||||
* @requires $route
|
||||
*
|
||||
* @description
|
||||
* The `$routeParams` service allows you to retrieve the current set of route parameters.
|
||||
*
|
||||
* Requires the {@link ngRoute `ngRoute`} module to be installed.
|
||||
*
|
||||
* The route parameters are a combination of {@link ng.$location `$location`}'s
|
||||
* {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}.
|
||||
* The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched.
|
||||
*
|
||||
* In case of parameter name collision, `path` params take precedence over `search` params.
|
||||
*
|
||||
* The service guarantees that the identity of the `$routeParams` object will remain unchanged
|
||||
* (but its properties will likely change) even when a route change occurs.
|
||||
*
|
||||
* Note that the `$routeParams` are only updated *after* a route change completes successfully.
|
||||
* This means that you cannot rely on `$routeParams` being correct in route resolve functions.
|
||||
* Instead you can use `$route.current.params` to access the new route's parameters.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* // Given:
|
||||
* // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
|
||||
* // Route: /Chapter/:chapterId/Section/:sectionId
|
||||
* //
|
||||
* // Then
|
||||
* $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'}
|
||||
* ```
|
||||
*/
|
||||
function $RouteParamsProvider() {
|
||||
this.$get = function() { return {}; };
|
||||
}
|
||||
|
||||
ngRouteModule.directive('ngView', ngViewFactory);
|
||||
ngRouteModule.directive('ngView', ngViewFillContentFactory);
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngView
|
||||
* @restrict ECA
|
||||
*
|
||||
* @description
|
||||
* # Overview
|
||||
* `ngView` is a directive that complements the {@link ngRoute.$route $route} service by
|
||||
* including the rendered template of the current route into the main layout (`index.html`) file.
|
||||
* Every time the current route changes, the included view changes with it according to the
|
||||
* configuration of the `$route` service.
|
||||
*
|
||||
* Requires the {@link ngRoute `ngRoute`} module to be installed.
|
||||
*
|
||||
* @animations
|
||||
* enter - animation is used to bring new content into the browser.
|
||||
* leave - animation is used to animate existing content away.
|
||||
*
|
||||
* The enter and leave animation occur concurrently.
|
||||
*
|
||||
* @scope
|
||||
* @priority 400
|
||||
* @param {string=} onload Expression to evaluate whenever the view updates.
|
||||
*
|
||||
* @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll
|
||||
* $anchorScroll} to scroll the viewport after the view is updated.
|
||||
*
|
||||
* - If the attribute is not set, disable scrolling.
|
||||
* - If the attribute is set without value, enable scrolling.
|
||||
* - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated
|
||||
* as an expression yields a truthy value.
|
||||
* @example
|
||||
<example name="ngView-directive" module="ngViewExample"
|
||||
deps="angular-route.js;angular-animate.js"
|
||||
animations="true" fixBase="true">
|
||||
<file name="index.html">
|
||||
<div ng-controller="MainCtrl as main">
|
||||
Choose:
|
||||
<a href="Book/Moby">Moby</a> |
|
||||
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
|
||||
<a href="Book/Gatsby">Gatsby</a> |
|
||||
<a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
|
||||
<a href="Book/Scarlet">Scarlet Letter</a><br/>
|
||||
|
||||
<div class="view-animate-container">
|
||||
<div ng-view class="view-animate"></div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<pre>$location.path() = {{main.$location.path()}}</pre>
|
||||
<pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre>
|
||||
<pre>$route.current.params = {{main.$route.current.params}}</pre>
|
||||
<pre>$routeParams = {{main.$routeParams}}</pre>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="book.html">
|
||||
<div>
|
||||
controller: {{book.name}}<br />
|
||||
Book Id: {{book.params.bookId}}<br />
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="chapter.html">
|
||||
<div>
|
||||
controller: {{chapter.name}}<br />
|
||||
Book Id: {{chapter.params.bookId}}<br />
|
||||
Chapter Id: {{chapter.params.chapterId}}
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="animations.css">
|
||||
.view-animate-container {
|
||||
position:relative;
|
||||
height:100px!important;
|
||||
position:relative;
|
||||
background:white;
|
||||
border:1px solid black;
|
||||
height:40px;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.view-animate {
|
||||
padding:10px;
|
||||
}
|
||||
|
||||
.view-animate.ng-enter, .view-animate.ng-leave {
|
||||
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
|
||||
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
|
||||
|
||||
display:block;
|
||||
width:100%;
|
||||
border-left:1px solid black;
|
||||
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
padding:10px;
|
||||
}
|
||||
|
||||
.view-animate.ng-enter {
|
||||
left:100%;
|
||||
}
|
||||
.view-animate.ng-enter.ng-enter-active {
|
||||
left:0;
|
||||
}
|
||||
.view-animate.ng-leave.ng-leave-active {
|
||||
left:-100%;
|
||||
}
|
||||
</file>
|
||||
|
||||
<file name="script.js">
|
||||
angular.module('ngViewExample', ['ngRoute', 'ngAnimate'])
|
||||
.config(['$routeProvider', '$locationProvider',
|
||||
function($routeProvider, $locationProvider) {
|
||||
$routeProvider
|
||||
.when('/Book/:bookId', {
|
||||
templateUrl: 'book.html',
|
||||
controller: 'BookCtrl',
|
||||
controllerAs: 'book'
|
||||
})
|
||||
.when('/Book/:bookId/ch/:chapterId', {
|
||||
templateUrl: 'chapter.html',
|
||||
controller: 'ChapterCtrl',
|
||||
controllerAs: 'chapter'
|
||||
});
|
||||
|
||||
// configure html5 to get links working on jsfiddle
|
||||
$locationProvider.html5Mode(true);
|
||||
}])
|
||||
.controller('MainCtrl', ['$route', '$routeParams', '$location',
|
||||
function($route, $routeParams, $location) {
|
||||
this.$route = $route;
|
||||
this.$location = $location;
|
||||
this.$routeParams = $routeParams;
|
||||
}])
|
||||
.controller('BookCtrl', ['$routeParams', function($routeParams) {
|
||||
this.name = "BookCtrl";
|
||||
this.params = $routeParams;
|
||||
}])
|
||||
.controller('ChapterCtrl', ['$routeParams', function($routeParams) {
|
||||
this.name = "ChapterCtrl";
|
||||
this.params = $routeParams;
|
||||
}]);
|
||||
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should load and compile correct template', function() {
|
||||
element(by.linkText('Moby: Ch1')).click();
|
||||
var content = element(by.css('[ng-view]')).getText();
|
||||
expect(content).toMatch(/controller\: ChapterCtrl/);
|
||||
expect(content).toMatch(/Book Id\: Moby/);
|
||||
expect(content).toMatch(/Chapter Id\: 1/);
|
||||
|
||||
element(by.partialLinkText('Scarlet')).click();
|
||||
|
||||
content = element(by.css('[ng-view]')).getText();
|
||||
expect(content).toMatch(/controller\: BookCtrl/);
|
||||
expect(content).toMatch(/Book Id\: Scarlet/);
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc event
|
||||
* @name ngView#$viewContentLoaded
|
||||
* @eventType emit on the current ngView scope
|
||||
* @description
|
||||
* Emitted every time the ngView content is reloaded.
|
||||
*/
|
||||
ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate'];
|
||||
function ngViewFactory( $route, $anchorScroll, $animate) {
|
||||
return {
|
||||
restrict: 'ECA',
|
||||
terminal: true,
|
||||
priority: 400,
|
||||
transclude: 'element',
|
||||
link: function(scope, $element, attr, ctrl, $transclude) {
|
||||
var currentScope,
|
||||
currentElement,
|
||||
previousElement,
|
||||
autoScrollExp = attr.autoscroll,
|
||||
onloadExp = attr.onload || '';
|
||||
|
||||
scope.$on('$routeChangeSuccess', update);
|
||||
update();
|
||||
|
||||
function cleanupLastView() {
|
||||
if(previousElement) {
|
||||
previousElement.remove();
|
||||
previousElement = null;
|
||||
}
|
||||
if(currentScope) {
|
||||
currentScope.$destroy();
|
||||
currentScope = null;
|
||||
}
|
||||
if(currentElement) {
|
||||
$animate.leave(currentElement).then(function() {
|
||||
previousElement = null;
|
||||
});
|
||||
previousElement = currentElement;
|
||||
currentElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
var locals = $route.current && $route.current.locals,
|
||||
template = locals && locals.$template;
|
||||
|
||||
if (angular.isDefined(template)) {
|
||||
var newScope = scope.$new();
|
||||
var current = $route.current;
|
||||
|
||||
// Note: This will also link all children of ng-view that were contained in the original
|
||||
// html. If that content contains controllers, ... they could pollute/change the scope.
|
||||
// However, using ng-view on an element with additional content does not make sense...
|
||||
// Note: We can't remove them in the cloneAttchFn of $transclude as that
|
||||
// function is called before linking the content, which would apply child
|
||||
// directives to non existing elements.
|
||||
var clone = $transclude(newScope, function(clone) {
|
||||
$animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter () {
|
||||
if (angular.isDefined(autoScrollExp)
|
||||
&& (!autoScrollExp || scope.$eval(autoScrollExp))) {
|
||||
$anchorScroll();
|
||||
}
|
||||
});
|
||||
cleanupLastView();
|
||||
});
|
||||
|
||||
currentElement = clone;
|
||||
currentScope = current.scope = newScope;
|
||||
currentScope.$emit('$viewContentLoaded');
|
||||
currentScope.$eval(onloadExp);
|
||||
} else {
|
||||
cleanupLastView();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// This directive is called during the $transclude call of the first `ngView` directive.
|
||||
// It will replace and compile the content of the element with the loaded template.
|
||||
// We need this directive so that the element content is already filled when
|
||||
// the link function of another directive on the same element as ngView
|
||||
// is called.
|
||||
ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route'];
|
||||
function ngViewFillContentFactory($compile, $controller, $route) {
|
||||
return {
|
||||
restrict: 'ECA',
|
||||
priority: -400,
|
||||
link: function(scope, $element) {
|
||||
var current = $route.current,
|
||||
locals = current.locals;
|
||||
|
||||
$element.html(locals.$template);
|
||||
|
||||
var link = $compile($element.contents());
|
||||
|
||||
if (current.controller) {
|
||||
locals.$scope = scope;
|
||||
var controller = $controller(current.controller, locals);
|
||||
if (current.controllerAs) {
|
||||
scope[current.controllerAs] = controller;
|
||||
}
|
||||
$element.data('$ngControllerController', controller);
|
||||
$element.children().data('$ngControllerController', controller);
|
||||
}
|
||||
|
||||
link(scope);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
})(window, window.angular);
|
14
syweb/webclient/js/angular-route.min.js
vendored
@ -1,14 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.3.0-rc.1
|
||||
(c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(r,d,z){'use strict';function v(s,h,f){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,e,b,g,u){function w(){k&&(k.remove(),k=null);l&&(l.$destroy(),l=null);n&&(f.leave(n).then(function(){k=null}),k=n,n=null)}function t(){var c=s.current&&s.current.locals;if(d.isDefined(c&&c.$template)){var c=a.$new(),m=s.current;n=u(c,function(c){f.enter(c,null,n||e).then(function(){!d.isDefined(p)||p&&!a.$eval(p)||h()});w()});l=m.scope=c;l.$emit("$viewContentLoaded");
|
||||
l.$eval(q)}else w()}var l,n,k,p=b.autoscroll,q=b.onload||"";a.$on("$routeChangeSuccess",t);t()}}}function x(d,h,f){return{restrict:"ECA",priority:-400,link:function(a,e){var b=f.current,g=b.locals;e.html(g.$template);var u=d(e.contents());b.controller&&(g.$scope=a,g=h(b.controller,g),b.controllerAs&&(a[b.controllerAs]=g),e.data("$ngControllerController",g),e.children().data("$ngControllerController",g));u(a)}}}r=d.module("ngRoute",["ng"]).provider("$route",function(){function s(a,e){return d.extend(new (d.extend(function(){},
|
||||
{prototype:a})),e)}function h(a,d){var b=d.caseInsensitiveMatch,g={originalPath:a,regexp:a},f=g.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,d,e,b){a="?"===b?b:null;b="*"===b?b:null;f.push({name:e,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(b&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");g.regexp=new RegExp("^"+a+"$",b?"i":"");return g}var f={};this.when=function(a,e){f[a]=d.extend({reloadOnSearch:!0},e,a&&h(a,e));if(a){var b=
|
||||
"/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";f[b]=d.extend({redirectTo:a},h(b,e))}return this};this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,e,b,g,h,r,t){function l(){var c=n(),m=q.current;if(c&&m&&c.$$route===m.$$route&&d.equals(c.pathParams,m.pathParams)&&!c.reloadOnSearch&&!p)m.params=c.params,d.copy(m.params,b),a.$broadcast("$routeUpdate",
|
||||
m);else if(c||m)p=!1,a.$broadcast("$routeChangeStart",c,m),(q.current=c)&&c.redirectTo&&(d.isString(c.redirectTo)?e.path(k(c.redirectTo,c.params)).search(c.params).replace():e.url(c.redirectTo(c.pathParams,e.path(),e.search())).replace()),g.when(c).then(function(){if(c){var a=d.extend({},c.resolve),e,b;d.forEach(a,function(c,b){a[b]=d.isString(c)?h.get(c):h.invoke(c,null,null,b)});d.isDefined(e=c.template)?d.isFunction(e)&&(e=e(c.params)):d.isDefined(b=c.templateUrl)&&(d.isFunction(b)&&(b=b(c.params)),
|
||||
b=t.getTrustedResourceUrl(b),d.isDefined(b)&&(c.loadedTemplateUrl=b,e=r(b)));d.isDefined(e)&&(a.$template=e);return g.all(a)}}).then(function(e){c==q.current&&(c&&(c.locals=e,d.copy(c.params,b)),a.$broadcast("$routeChangeSuccess",c,m))},function(d){c==q.current&&a.$broadcast("$routeChangeError",c,m,d)})}function n(){var c,a;d.forEach(f,function(b,g){var f;if(f=!a){var h=e.path();f=b.keys;var l={};if(b.regexp)if(h=b.regexp.exec(h)){for(var k=1,n=h.length;k<n;++k){var p=f[k-1],q=h[k];p&&q&&(l[p.name]=
|
||||
q)}f=l}else f=null;else f=null;f=c=f}f&&(a=s(b,{params:d.extend({},e.search(),c),pathParams:c}),a.$$route=b)});return a||f[null]&&s(f[null],{params:{},pathParams:{}})}function k(a,b){var e=[];d.forEach((a||"").split(":"),function(a,c){if(0===c)e.push(a);else{var d=a.match(/(\w+)(.*)/),f=d[1];e.push(b[f]);e.push(d[2]||"");delete b[f]}});return e.join("")}var p=!1,q={routes:f,reload:function(){p=!0;a.$evalAsync(l)},updateParams:function(a){if(this.current&&this.current.$$route){var b={},f=this;d.forEach(Object.keys(a),
|
||||
function(d){f.current.pathParams[d]||(b[d]=a[d])});a=d.extend({},this.current.params,a);e.path(k(this.current.$$route.originalPath,a));e.search(d.extend({},e.search(),b))}else throw y("norout");}};a.$on("$locationChangeSuccess",l);return q}]});var y=d.$$minErr("ngRoute");r.provider("$routeParams",function(){this.$get=function(){return{}}});r.directive("ngView",v);r.directive("ngView",x);v.$inject=["$route","$anchorScroll","$animate"];x.$inject=["$compile","$controller","$route"]})(window,window.angular);
|
||||
//# sourceMappingURL=angular-route.min.js.map
|
647
syweb/webclient/js/angular-sanitize.js
vendored
@ -1,647 +0,0 @@
|
||||
/**
|
||||
* @license AngularJS v1.3.0-rc.1
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window, angular, undefined) {'use strict';
|
||||
|
||||
var $sanitizeMinErr = angular.$$minErr('$sanitize');
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
* @name ngSanitize
|
||||
* @description
|
||||
*
|
||||
* # ngSanitize
|
||||
*
|
||||
* The `ngSanitize` module provides functionality to sanitize HTML.
|
||||
*
|
||||
*
|
||||
* <div doc-module-components="ngSanitize"></div>
|
||||
*
|
||||
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
|
||||
*/
|
||||
|
||||
/*
|
||||
* HTML Parser By Misko Hevery (misko@hevery.com)
|
||||
* based on: HTML Parser By John Resig (ejohn.org)
|
||||
* Original code by Erik Arvidsson, Mozilla Public License
|
||||
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
||||
*
|
||||
* // Use like so:
|
||||
* htmlParser(htmlString, {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* });
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $sanitize
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
|
||||
* then serialized back to properly escaped html string. This means that no unsafe input can make
|
||||
* it into the returned string, however, since our parser is more strict than a typical browser
|
||||
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a
|
||||
* browser, won't make it through the sanitizer.
|
||||
* The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
|
||||
* `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
|
||||
*
|
||||
* @param {string} html Html input.
|
||||
* @returns {string} Sanitized html.
|
||||
*
|
||||
* @example
|
||||
<example module="sanitizeExample" deps="angular-sanitize.js">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('sanitizeExample', ['ngSanitize'])
|
||||
.controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
|
||||
$scope.snippet =
|
||||
'<p style="color:blue">an html\n' +
|
||||
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
|
||||
'snippet</p>';
|
||||
$scope.deliberatelyTrustDangerousSnippet = function() {
|
||||
return $sce.trustAsHtml($scope.snippet);
|
||||
};
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Directive</td>
|
||||
<td>How</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="bind-html-with-sanitize">
|
||||
<td>ng-bind-html</td>
|
||||
<td>Automatically uses $sanitize</td>
|
||||
<td><pre><div ng-bind-html="snippet"><br/></div></pre></td>
|
||||
<td><div ng-bind-html="snippet"></div></td>
|
||||
</tr>
|
||||
<tr id="bind-html-with-trust">
|
||||
<td>ng-bind-html</td>
|
||||
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()">
|
||||
</div></pre>
|
||||
</td>
|
||||
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
|
||||
</tr>
|
||||
<tr id="bind-default">
|
||||
<td>ng-bind</td>
|
||||
<td>Automatically escapes</td>
|
||||
<td><pre><div ng-bind="snippet"><br/></div></pre></td>
|
||||
<td><div ng-bind="snippet"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should sanitize the html snippet by default', function() {
|
||||
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
|
||||
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
|
||||
});
|
||||
|
||||
it('should inline raw snippet if bound to a trusted value', function() {
|
||||
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
|
||||
toBe("<p style=\"color:blue\">an html\n" +
|
||||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||||
"snippet</p>");
|
||||
});
|
||||
|
||||
it('should escape snippet without any filter', function() {
|
||||
expect(element(by.css('#bind-default div')).getInnerHtml()).
|
||||
toBe("<p style=\"color:blue\">an html\n" +
|
||||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||||
"snippet</p>");
|
||||
});
|
||||
|
||||
it('should update', function() {
|
||||
element(by.model('snippet')).clear();
|
||||
element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
|
||||
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
|
||||
toBe('new <b>text</b>');
|
||||
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
|
||||
'new <b onclick="alert(1)">text</b>');
|
||||
expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
|
||||
"new <b onclick=\"alert(1)\">text</b>");
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
function $SanitizeProvider() {
|
||||
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
|
||||
return function(html) {
|
||||
var buf = [];
|
||||
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
|
||||
return !/^unsafe/.test($$sanitizeUri(uri, isImage));
|
||||
}));
|
||||
return buf.join('');
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
function sanitizeText(chars) {
|
||||
var buf = [];
|
||||
var writer = htmlSanitizeWriter(buf, angular.noop);
|
||||
writer.chars(chars);
|
||||
return buf.join('');
|
||||
}
|
||||
|
||||
|
||||
// Regular Expressions for parsing tags and attributes
|
||||
var START_TAG_REGEXP =
|
||||
/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
|
||||
END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
|
||||
ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
|
||||
BEGIN_TAG_REGEXP = /^</,
|
||||
BEGING_END_TAGE_REGEXP = /^<\//,
|
||||
COMMENT_REGEXP = /<!--(.*?)-->/g,
|
||||
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
|
||||
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
|
||||
SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
// Match everything outside of normal chars and " (quote character)
|
||||
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
|
||||
|
||||
|
||||
// Good source of info about elements and attributes
|
||||
// http://dev.w3.org/html5/spec/Overview.html#semantics
|
||||
// http://simon.html5.org/html-elements
|
||||
|
||||
// Safe Void Elements - HTML5
|
||||
// http://dev.w3.org/html5/spec/Overview.html#void-elements
|
||||
var voidElements = makeMap("area,br,col,hr,img,wbr");
|
||||
|
||||
// Elements that you can, intentionally, leave open (and which close themselves)
|
||||
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
|
||||
var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
|
||||
optionalEndTagInlineElements = makeMap("rp,rt"),
|
||||
optionalEndTagElements = angular.extend({},
|
||||
optionalEndTagInlineElements,
|
||||
optionalEndTagBlockElements);
|
||||
|
||||
// Safe Block Elements - HTML5
|
||||
var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
|
||||
"aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
|
||||
"h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
|
||||
|
||||
// Inline Elements - HTML5
|
||||
var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
|
||||
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
|
||||
"samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
|
||||
|
||||
|
||||
// Special Elements (can contain anything)
|
||||
var specialElements = makeMap("script,style");
|
||||
|
||||
var validElements = angular.extend({},
|
||||
voidElements,
|
||||
blockElements,
|
||||
inlineElements,
|
||||
optionalEndTagElements);
|
||||
|
||||
//Attributes that have href and hence need to be sanitized
|
||||
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
|
||||
var validAttrs = angular.extend({}, uriAttrs, makeMap(
|
||||
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
|
||||
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
|
||||
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
|
||||
'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
|
||||
'valign,value,vspace,width'));
|
||||
|
||||
function makeMap(str) {
|
||||
var obj = {}, items = str.split(','), i;
|
||||
for (i = 0; i < items.length; i++) obj[items[i]] = true;
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @example
|
||||
* htmlParser(htmlString, {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* });
|
||||
*
|
||||
* @param {string} html string
|
||||
* @param {object} handler
|
||||
*/
|
||||
function htmlParser( html, handler ) {
|
||||
if (typeof html !== 'string') {
|
||||
if (html === null || typeof html === 'undefined') {
|
||||
html = '';
|
||||
} else {
|
||||
html = '' + html;
|
||||
}
|
||||
}
|
||||
var index, chars, match, stack = [], last = html, text;
|
||||
stack.last = function() { return stack[ stack.length - 1 ]; };
|
||||
|
||||
while ( html ) {
|
||||
text = '';
|
||||
chars = true;
|
||||
|
||||
// Make sure we're not in a script or style element
|
||||
if ( !stack.last() || !specialElements[ stack.last() ] ) {
|
||||
|
||||
// Comment
|
||||
if ( html.indexOf("<!--") === 0 ) {
|
||||
// comments containing -- are not allowed unless they terminate the comment
|
||||
index = html.indexOf("--", 4);
|
||||
|
||||
if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
|
||||
if (handler.comment) handler.comment( html.substring( 4, index ) );
|
||||
html = html.substring( index + 3 );
|
||||
chars = false;
|
||||
}
|
||||
// DOCTYPE
|
||||
} else if ( DOCTYPE_REGEXP.test(html) ) {
|
||||
match = html.match( DOCTYPE_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
html = html.replace( match[0], '');
|
||||
chars = false;
|
||||
}
|
||||
// end tag
|
||||
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
|
||||
match = html.match( END_TAG_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
html = html.substring( match[0].length );
|
||||
match[0].replace( END_TAG_REGEXP, parseEndTag );
|
||||
chars = false;
|
||||
}
|
||||
|
||||
// start tag
|
||||
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
|
||||
match = html.match( START_TAG_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
// We only have a valid start-tag if there is a '>'.
|
||||
if ( match[4] ) {
|
||||
html = html.substring( match[0].length );
|
||||
match[0].replace( START_TAG_REGEXP, parseStartTag );
|
||||
}
|
||||
chars = false;
|
||||
} else {
|
||||
// no ending tag found --- this piece should be encoded as an entity.
|
||||
text += '<';
|
||||
html = html.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
if ( chars ) {
|
||||
index = html.indexOf("<");
|
||||
|
||||
text += index < 0 ? html : html.substring( 0, index );
|
||||
html = index < 0 ? "" : html.substring( index );
|
||||
|
||||
if (handler.chars) handler.chars( decodeEntities(text) );
|
||||
}
|
||||
|
||||
} else {
|
||||
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
|
||||
function(all, text){
|
||||
text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
|
||||
|
||||
if (handler.chars) handler.chars( decodeEntities(text) );
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
parseEndTag( "", stack.last() );
|
||||
}
|
||||
|
||||
if ( html == last ) {
|
||||
throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
|
||||
"of html: {0}", html);
|
||||
}
|
||||
last = html;
|
||||
}
|
||||
|
||||
// Clean up any remaining tags
|
||||
parseEndTag();
|
||||
|
||||
function parseStartTag( tag, tagName, rest, unary ) {
|
||||
tagName = angular.lowercase(tagName);
|
||||
if ( blockElements[ tagName ] ) {
|
||||
while ( stack.last() && inlineElements[ stack.last() ] ) {
|
||||
parseEndTag( "", stack.last() );
|
||||
}
|
||||
}
|
||||
|
||||
if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
|
||||
parseEndTag( "", tagName );
|
||||
}
|
||||
|
||||
unary = voidElements[ tagName ] || !!unary;
|
||||
|
||||
if ( !unary )
|
||||
stack.push( tagName );
|
||||
|
||||
var attrs = {};
|
||||
|
||||
rest.replace(ATTR_REGEXP,
|
||||
function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
|
||||
var value = doubleQuotedValue
|
||||
|| singleQuotedValue
|
||||
|| unquotedValue
|
||||
|| '';
|
||||
|
||||
attrs[name] = decodeEntities(value);
|
||||
});
|
||||
if (handler.start) handler.start( tagName, attrs, unary );
|
||||
}
|
||||
|
||||
function parseEndTag( tag, tagName ) {
|
||||
var pos = 0, i;
|
||||
tagName = angular.lowercase(tagName);
|
||||
if ( tagName )
|
||||
// Find the closest opened tag of the same type
|
||||
for ( pos = stack.length - 1; pos >= 0; pos-- )
|
||||
if ( stack[ pos ] == tagName )
|
||||
break;
|
||||
|
||||
if ( pos >= 0 ) {
|
||||
// Close all the open elements, up the stack
|
||||
for ( i = stack.length - 1; i >= pos; i-- )
|
||||
if (handler.end) handler.end( stack[ i ] );
|
||||
|
||||
// Remove the open elements from the stack
|
||||
stack.length = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hiddenPre=document.createElement("pre");
|
||||
var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
|
||||
/**
|
||||
* decodes all entities into regular string
|
||||
* @param value
|
||||
* @returns {string} A string with decoded entities.
|
||||
*/
|
||||
function decodeEntities(value) {
|
||||
if (!value) { return ''; }
|
||||
|
||||
// Note: IE8 does not preserve spaces at the start/end of innerHTML
|
||||
// so we must capture them and reattach them afterward
|
||||
var parts = spaceRe.exec(value);
|
||||
var spaceBefore = parts[1];
|
||||
var spaceAfter = parts[3];
|
||||
var content = parts[2];
|
||||
if (content) {
|
||||
hiddenPre.innerHTML=content.replace(/</g,"<");
|
||||
// innerText depends on styling as it doesn't display hidden elements.
|
||||
// Therefore, it's better to use textContent not to cause unnecessary
|
||||
// reflows. However, IE<9 don't support textContent so the innerText
|
||||
// fallback is necessary.
|
||||
content = 'textContent' in hiddenPre ?
|
||||
hiddenPre.textContent : hiddenPre.innerText;
|
||||
}
|
||||
return spaceBefore + content + spaceAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes all potentially dangerous characters, so that the
|
||||
* resulting string can be safely inserted into attribute or
|
||||
* element text.
|
||||
* @param value
|
||||
* @returns {string} escaped text
|
||||
*/
|
||||
function encodeEntities(value) {
|
||||
return value.
|
||||
replace(/&/g, '&').
|
||||
replace(SURROGATE_PAIR_REGEXP, function (value) {
|
||||
var hi = value.charCodeAt(0);
|
||||
var low = value.charCodeAt(1);
|
||||
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
|
||||
}).
|
||||
replace(NON_ALPHANUMERIC_REGEXP, function(value){
|
||||
return '&#' + value.charCodeAt(0) + ';';
|
||||
}).
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* create an HTML/XML writer which writes to buffer
|
||||
* @param {Array} buf use buf.jain('') to get out sanitized html string
|
||||
* @returns {object} in the form of {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* }
|
||||
*/
|
||||
function htmlSanitizeWriter(buf, uriValidator){
|
||||
var ignore = false;
|
||||
var out = angular.bind(buf, buf.push);
|
||||
return {
|
||||
start: function(tag, attrs, unary){
|
||||
tag = angular.lowercase(tag);
|
||||
if (!ignore && specialElements[tag]) {
|
||||
ignore = tag;
|
||||
}
|
||||
if (!ignore && validElements[tag] === true) {
|
||||
out('<');
|
||||
out(tag);
|
||||
angular.forEach(attrs, function(value, key){
|
||||
var lkey=angular.lowercase(key);
|
||||
var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
|
||||
if (validAttrs[lkey] === true &&
|
||||
(uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
|
||||
out(' ');
|
||||
out(key);
|
||||
out('="');
|
||||
out(encodeEntities(value));
|
||||
out('"');
|
||||
}
|
||||
});
|
||||
out(unary ? '/>' : '>');
|
||||
}
|
||||
},
|
||||
end: function(tag){
|
||||
tag = angular.lowercase(tag);
|
||||
if (!ignore && validElements[tag] === true) {
|
||||
out('</');
|
||||
out(tag);
|
||||
out('>');
|
||||
}
|
||||
if (tag == ignore) {
|
||||
ignore = false;
|
||||
}
|
||||
},
|
||||
chars: function(chars){
|
||||
if (!ignore) {
|
||||
out(encodeEntities(chars));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// define ngSanitize module and register $sanitize service
|
||||
angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
||||
|
||||
/* global sanitizeText: false */
|
||||
|
||||
/**
|
||||
* @ngdoc filter
|
||||
* @name linky
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
|
||||
* plain email address links.
|
||||
*
|
||||
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
|
||||
*
|
||||
* @param {string} text Input text.
|
||||
* @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
|
||||
* @returns {string} Html-linkified text.
|
||||
*
|
||||
* @usage
|
||||
<span ng-bind-html="linky_expression | linky"></span>
|
||||
*
|
||||
* @example
|
||||
<example module="linkyExample" deps="angular-sanitize.js">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('linkyExample', ['ngSanitize'])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.snippet =
|
||||
'Pretty text with some links:\n'+
|
||||
'http://angularjs.org/,\n'+
|
||||
'mailto:us@somewhere.org,\n'+
|
||||
'another@somewhere.org,\n'+
|
||||
'and one more: ftp://127.0.0.1/.';
|
||||
$scope.snippetWithTarget = 'http://angularjs.org/';
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Filter</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="linky-filter">
|
||||
<td>linky filter</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippet | linky"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippet | linky"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="linky-target">
|
||||
<td>linky target</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="escaped-html">
|
||||
<td>no filter</td>
|
||||
<td><pre><div ng-bind="snippet"><br></div></pre></td>
|
||||
<td><div ng-bind="snippet"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should linkify the snippet with urls', function() {
|
||||
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
||||
toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
|
||||
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
|
||||
expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
|
||||
});
|
||||
|
||||
it('should not linkify snippet without the linky filter', function() {
|
||||
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
|
||||
toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
|
||||
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
|
||||
expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update', function() {
|
||||
element(by.model('snippet')).clear();
|
||||
element(by.model('snippet')).sendKeys('new http://link.');
|
||||
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
||||
toBe('new http://link.');
|
||||
expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
|
||||
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
|
||||
.toBe('new http://link.');
|
||||
});
|
||||
|
||||
it('should work with the target property', function() {
|
||||
expect(element(by.id('linky-target')).
|
||||
element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
|
||||
toBe('http://angularjs.org/');
|
||||
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
|
||||
var LINKY_URL_REGEXP =
|
||||
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
|
||||
MAILTO_REGEXP = /^mailto:/;
|
||||
|
||||
return function(text, target) {
|
||||
if (!text) return text;
|
||||
var match;
|
||||
var raw = text;
|
||||
var html = [];
|
||||
var url;
|
||||
var i;
|
||||
while ((match = raw.match(LINKY_URL_REGEXP))) {
|
||||
// We can not end in these as they are sometimes found at the end of the sentence
|
||||
url = match[0];
|
||||
// if we did not match ftp/http/mailto then assume mailto
|
||||
if (match[2] == match[3]) url = 'mailto:' + url;
|
||||
i = match.index;
|
||||
addText(raw.substr(0, i));
|
||||
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
|
||||
raw = raw.substring(i + match[0].length);
|
||||
}
|
||||
addText(raw);
|
||||
return $sanitize(html.join(''));
|
||||
|
||||
function addText(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
html.push(sanitizeText(text));
|
||||
}
|
||||
|
||||
function addLink(url, text) {
|
||||
html.push('<a ');
|
||||
if (angular.isDefined(target)) {
|
||||
html.push('target="');
|
||||
html.push(target);
|
||||
html.push('" ');
|
||||
}
|
||||
html.push('href="');
|
||||
html.push(url);
|
||||
html.push('">');
|
||||
addText(text);
|
||||
html.push('</a>');
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
|
||||
})(window, window.angular);
|
15
syweb/webclient/js/angular-sanitize.min.js
vendored
@ -1,15 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.3.0-rc.1
|
||||
(c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(q,g,r){'use strict';function F(a){var d=[];t(d,g.noop).chars(a);return d.join("")}function m(a){var d={};a=a.split(",");var c;for(c=0;c<a.length;c++)d[a[c]]=!0;return d}function G(a,d){function c(a,b,c,h){b=g.lowercase(b);if(u[b])for(;f.last()&&v[f.last()];)e("",f.last());w[b]&&f.last()==b&&e("",b);(h=x[b]||!!h)||f.push(b);var n={};c.replace(H,function(a,b,d,c,e){n[b]=s(d||c||e||"")});d.start&&d.start(b,n,h)}function e(a,b){var c=0,e;if(b=g.lowercase(b))for(c=f.length-1;0<=c&&f[c]!=b;c--);
|
||||
if(0<=c){for(e=f.length-1;e>=c;e--)d.end&&d.end(f[e]);f.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,l,f=[],n=a,h;for(f.last=function(){return f[f.length-1]};a;){h="";l=!0;if(f.last()&&y[f.last()])a=a.replace(new RegExp("(.*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(a,b){b=b.replace(I,"$1").replace(J,"$1");d.chars&&d.chars(s(b));return""}),e("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(d.comment&&d.comment(a.substring(4,
|
||||
b)),a=a.substring(b+3),l=!1);else if(z.test(a)){if(b=a.match(z))a=a.replace(b[0],""),l=!1}else if(K.test(a)){if(b=a.match(A))a=a.substring(b[0].length),b[0].replace(A,e),l=!1}else L.test(a)&&((b=a.match(B))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(B,c)),l=!1):(h+="<",a=a.substring(1)));l&&(b=a.indexOf("<"),h+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),d.chars&&d.chars(s(h)))}if(a==n)throw M("badparse",a);n=a}e()}function s(a){if(!a)return"";var d=N.exec(a);a=d[1];var c=d[3];if(d=d[2])p.innerHTML=
|
||||
d.replace(/</g,"<"),d="textContent"in p?p.textContent:p.innerText;return a+d+c}function C(a){return a.replace(/&/g,"&").replace(O,function(a){var c=a.charCodeAt(0);a=a.charCodeAt(1);return"&#"+(1024*(c-55296)+(a-56320)+65536)+";"}).replace(P,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"<").replace(/>/g,">")}function t(a,d){var c=!1,e=g.bind(a,a.push);return{start:function(a,l,f){a=g.lowercase(a);!c&&y[a]&&(c=a);c||!0!==D[a]||(e("<"),e(a),g.forEach(l,function(c,f){var k=
|
||||
g.lowercase(f),l="img"===a&&"src"===k||"background"===k;!0!==Q[k]||!0===E[k]&&!d(c,l)||(e(" "),e(f),e('="'),e(C(c)),e('"'))}),e(f?"/>":">"))},end:function(a){a=g.lowercase(a);c||!0!==D[a]||(e("</"),e(a),e(">"));a==c&&(c=!1)},chars:function(a){c||e(C(a))}}}var M=g.$$minErr("$sanitize"),B=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,A=/^<\/\s*([\w:-]+)[^>]*>/,H=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,L=/^</,
|
||||
K=/^<\//,I=/\x3c!--(.*?)--\x3e/g,z=/<!DOCTYPE([^>]*?)>/i,J=/<!\[CDATA\[(.*?)]]\x3e/g,O=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,P=/([^\#-~| |!])/g,x=m("area,br,col,hr,img,wbr");q=m("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr");r=m("rp,rt");var w=g.extend({},r,q),u=g.extend({},q,m("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),v=g.extend({},r,m("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),
|
||||
y=m("script,style"),D=g.extend({},x,u,v,w),E=m("background,cite,href,longdesc,src,usemap"),Q=g.extend({},E,m("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width")),p=document.createElement("pre"),N=/^(\s*)([\s\S]*?)(\s*)$/;g.module("ngSanitize",[]).provider("$sanitize",
|
||||
function(){this.$get=["$$sanitizeUri",function(a){return function(d){var c=[];G(d,t(c,function(c,b){return!/^unsafe/.test(a(c,b))}));return c.join("")}}]});g.module("ngSanitize").filter("linky",["$sanitize",function(a){var d=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,c=/^mailto:/;return function(e,b){function l(a){a&&k.push(F(a))}function f(a,c){k.push("<a ");g.isDefined(b)&&(k.push('target="'),k.push(b),k.push('" '));k.push('href="');k.push(a);k.push('">');l(c);k.push("</a>")}
|
||||
if(!e)return e;for(var n,h=e,k=[],m,p;n=h.match(d);)m=n[0],n[2]==n[3]&&(m="mailto:"+m),p=n.index,l(h.substr(0,p)),f(m,n[0].replace(c,"")),h=h.substring(p+n[0].length);l(h);return a(k.join(""))}}])})(window,window.angular);
|
||||
//# sourceMappingURL=angular-sanitize.min.js.map
|
24417
syweb/webclient/js/angular.js
vendored
237
syweb/webclient/js/angular.min.js
vendored
@ -1,237 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.3.0-rc.1
|
||||
(c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(t,Y,s){'use strict';function K(b){return function(){var a=arguments[0],c;c="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.3.0-rc.1/"+(b?b+"/":"")+a;for(a=1;a<arguments.length;a++){c=c+(1==a?"?":"&")+"p"+(a-1)+"=";var d=encodeURIComponent,e;e=arguments[a];e="function"==typeof e?e.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof e?"undefined":"string"!=typeof e?JSON.stringify(e):e;c+=d(e)}return Error(c)}}function Na(b){if(null==b||Oa(b))return!1;var a=b.length;return 1===b.nodeType&&
|
||||
a?!0:C(b)||L(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function r(b,a,c){var d,e;if(b)if(D(b))for(d in b)"prototype"==d||"length"==d||"name"==d||b.hasOwnProperty&&!b.hasOwnProperty(d)||a.call(c,b[d],d,b);else if(L(b)||Na(b)){var f="object"!==typeof b;d=0;for(e=b.length;d<e;d++)(f||d in b)&&a.call(c,b[d],d,b)}else if(b.forEach&&b.forEach!==r)b.forEach(a,c,b);else for(d in b)b.hasOwnProperty(d)&&a.call(c,b[d],d,b);return b}function Zb(b){var a=[],c;for(c in b)b.hasOwnProperty(c)&&a.push(c);return a.sort()}
|
||||
function od(b,a,c){for(var d=Zb(b),e=0;e<d.length;e++)a.call(c,b[d[e]],d[e]);return d}function $b(b){return function(a,c){b(c,a)}}function pd(){return++bb}function ac(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function E(b){for(var a=b.$$hashKey,c=1,d=arguments.length;c<d;c++){var e=arguments[c];if(e)for(var f=Object.keys(e),g=0,h=f.length;g<h;g++){var m=f[g];b[m]=e[m]}}ac(b,a);return b}function U(b){return parseInt(b,10)}function bc(b,a){return E(new (E(function(){},{prototype:b})),a)}function w(){}
|
||||
function Pa(b){return b}function da(b){return function(){return b}}function F(b){return"undefined"===typeof b}function B(b){return"undefined"!==typeof b}function S(b){return null!==b&&"object"===typeof b}function C(b){return"string"===typeof b}function ea(b){return"number"===typeof b}function fa(b){return"[object Date]"===Ga.call(b)}function D(b){return"function"===typeof b}function cb(b){return"[object RegExp]"===Ga.call(b)}function Oa(b){return b&&b.window===b}function Qa(b){return b&&b.$evalAsync&&
|
||||
b.$watch}function qd(b){return!(!b||!(b.nodeName||b.prop&&b.attr&&b.find))}function rd(b){var a={};b=b.split(",");var c;for(c=0;c<b.length;c++)a[b[c]]=!0;return a}function pa(b){return P(b.nodeName||b[0].nodeName)}function sd(b,a,c){var d=[];r(b,function(b,f,g){d.push(a.call(c,b,f,g))});return d}function Ra(b,a){var c=b.indexOf(a);0<=c&&b.splice(c,1);return a}function Ha(b,a,c,d){if(Oa(b)||Qa(b))throw Sa("cpws");if(a){if(b===a)throw Sa("cpi");c=c||[];d=d||[];if(S(b)){var e=c.indexOf(b);if(-1!==e)return d[e];
|
||||
c.push(b);d.push(a)}if(L(b))for(var f=a.length=0;f<b.length;f++)e=Ha(b[f],null,c,d),S(b[f])&&(c.push(b[f]),d.push(e)),a.push(e);else{var g=a.$$hashKey;L(a)?a.length=0:r(a,function(c,b){delete a[b]});for(f in b)b.hasOwnProperty(f)&&(e=Ha(b[f],null,c,d),S(b[f])&&(c.push(b[f]),d.push(e)),a[f]=e);ac(a,g)}}else if(a=b)L(b)?a=Ha(b,[],c,d):fa(b)?a=new Date(b.getTime()):cb(b)?(a=new RegExp(b.source,b.toString().match(/[^\/]*$/)[0]),a.lastIndex=b.lastIndex):S(b)&&(e=Object.create(Object.getPrototypeOf(b)),
|
||||
a=Ha(b,e,c,d));return a}function qa(b,a){if(L(b)){a=a||[];for(var c=0,d=b.length;c<d;c++)a[c]=b[c]}else if(S(b))for(c in a=a||{},b)if("$"!==c.charAt(0)||"$"!==c.charAt(1))a[c]=b[c];return a||b}function ra(b,a){if(b===a)return!0;if(null===b||null===a)return!1;if(b!==b&&a!==a)return!0;var c=typeof b,d;if(c==typeof a&&"object"==c)if(L(b)){if(!L(a))return!1;if((c=b.length)==a.length){for(d=0;d<c;d++)if(!ra(b[d],a[d]))return!1;return!0}}else{if(fa(b))return fa(a)?ra(b.getTime(),a.getTime()):!1;if(cb(b)&&
|
||||
cb(a))return b.toString()==a.toString();if(Qa(b)||Qa(a)||Oa(b)||Oa(a)||L(a))return!1;c={};for(d in b)if("$"!==d.charAt(0)&&!D(b[d])){if(!ra(b[d],a[d]))return!1;c[d]=!0}for(d in a)if(!c.hasOwnProperty(d)&&"$"!==d.charAt(0)&&a[d]!==s&&!D(a[d]))return!1;return!0}return!1}function db(b,a,c){return b.concat(Ta.call(a,c))}function cc(b,a){var c=2<arguments.length?Ta.call(arguments,2):[];return!D(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?a.apply(b,c.concat(Ta.call(arguments,0))):
|
||||
a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function td(b,a){var c=a;"string"===typeof b&&"$"===b.charAt(0)&&"$"===b.charAt(1)?c=s:Oa(a)?c="$WINDOW":a&&Y===a?c="$DOCUMENT":Qa(a)&&(c="$SCOPE");return c}function sa(b,a){return"undefined"===typeof b?s:JSON.stringify(b,td,a?" ":null)}function dc(b){return C(b)?JSON.parse(b):b}function ta(b){b=G(b).clone();try{b.empty()}catch(a){}var c=G("<div>").append(b).html();try{return 3===b[0].nodeType?P(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,
|
||||
function(a,c){return"<"+P(c)})}catch(d){return P(c)}}function ec(b){try{return decodeURIComponent(b)}catch(a){}}function fc(b){var a={},c,d;r((b||"").split("&"),function(b){b&&(c=b.replace(/\+/g,"%20").split("="),d=ec(c[0]),B(d)&&(b=B(c[1])?ec(c[1]):!0,Ab.call(a,d)?L(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Bb(b){var a=[];r(b,function(b,d){L(b)?r(b,function(b){a.push(Da(d,!0)+(!0===b?"":"="+Da(b,!0)))}):a.push(Da(d,!0)+(!0===b?"":"="+Da(b,!0)))});return a.length?a.join("&"):""}
|
||||
function eb(b){return Da(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Da(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,a?"%20":"+")}function ud(b,a){var c,d,e=fb.length;b=G(b);for(d=0;d<e;++d)if(c=fb[d]+a,C(c=b.attr(c)))return c;return null}function vd(b,a){var c,d,e={};r(fb,function(a){a+="app";!c&&b.hasAttribute&&b.hasAttribute(a)&&(c=b,d=b.getAttribute(a))});
|
||||
r(fb,function(a){a+="app";var e;!c&&(e=b.querySelector("["+a.replace(":","\\:")+"]"))&&(c=e,d=e.getAttribute(a))});c&&(e.strictDi=null!==ud(c,"strict-di"),a(c,d?[d]:[],e))}function gc(b,a,c){S(c)||(c={});c=E({strictDi:!1},c);var d=function(){b=G(b);if(b.injector()){var d=b[0]===Y?"document":ta(b);throw Sa("btstrpd",d.replace(/</,"<").replace(/>/,">"));}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);c.debugInfoEnabled&&a.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);
|
||||
a.unshift("ng");d=Cb(a,c.strictDi);d.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return d},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;t&&e.test(t.name)&&(c.debugInfoEnabled=!0,t.name=t.name.replace(e,""));if(t&&!f.test(t.name))return d();t.name=t.name.replace(f,"");Ea.resumeBootstrap=function(b){r(b,function(b){a.push(b)});d()}}function wd(){t.name="NG_ENABLE_DEBUG_INFO!"+t.name;t.location.reload()}function xd(b){return Ea.element(b).injector().get("$$testability")}
|
||||
function Db(b,a){a=a||"_";return b.replace(yd,function(b,d){return(d?a:"")+b.toLowerCase()})}function zd(){var b;hc||((la=t.jQuery)&&la.fn.on?(G=la,E(la.fn,{scope:Ia.scope,isolateScope:Ia.isolateScope,controller:Ia.controller,injector:Ia.injector,inheritedData:Ia.inheritedData}),b=la.cleanData,la.cleanData=function(a){var c;if(Eb)Eb=!1;else for(var d=0,e;null!=(e=a[d]);d++)(c=la._data(e,"events"))&&c.$destroy&&la(e).triggerHandler("$destroy");b(a)}):G=V,Ea.element=G,hc=!0)}function Fb(b,a,c){if(!b)throw Sa("areq",
|
||||
a||"?",c||"required");return b}function gb(b,a,c){c&&L(b)&&(b=b[b.length-1]);Fb(D(b),a,"not a function, got "+(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function Ja(b,a){if("hasOwnProperty"===b)throw Sa("badname",a);}function ic(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,f=a.length,g=0;g<f;g++)d=a[g],b&&(b=(e=b)[d]);return!c&&D(b)?cc(e,b):b}function hb(b){var a=b[0];b=b[b.length-1];var c=[a];do{a=a.nextSibling;if(!a)break;c.push(a)}while(a!==b);return G(c)}function Ad(b){function a(a,
|
||||
b,c){return a[b]||(a[b]=c())}var c=K("$injector"),d=K("ng");b=a(b,"angular",Object);b.$$minErr=b.$$minErr||K;return a(b,"module",function(){var b={};return function(f,g,h){if("hasOwnProperty"===f)throw d("badname","module");g&&b.hasOwnProperty(f)&&(b[f]=null);return a(b,f,function(){function a(c,d,e,f){f||(f=b);return function(){f[e||"push"]([c,d,arguments]);return q}}if(!g)throw c("nomod",f);var b=[],d=[],e=[],l=a("$injector","invoke","push",d),q={_invokeQueue:b,_configBlocks:d,_runBlocks:e,requires:g,
|
||||
name:f,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:l,run:function(a){e.push(a);return this}};h&&l(h);return q})}})}function Bd(b){E(b,{bootstrap:gc,copy:Ha,extend:E,equals:ra,element:G,forEach:r,
|
||||
injector:Cb,noop:w,bind:cc,toJson:sa,fromJson:dc,identity:Pa,isUndefined:F,isDefined:B,isString:C,isFunction:D,isObject:S,isNumber:ea,isElement:qd,isArray:L,version:Cd,isDate:fa,lowercase:P,uppercase:ib,callbacks:{counter:0},getTestability:xd,$$minErr:K,$$csp:Ua,reloadWithDebugInfo:wd,$$hasClass:jb});Va=Ad(t);try{Va("ngLocale")}catch(a){Va("ngLocale",[]).provider("$locale",Dd)}Va("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:Ed});a.provider("$compile",jc).directive({a:Fd,input:kc,
|
||||
textarea:kc,form:Gd,script:Hd,select:Id,style:Jd,option:Kd,ngBind:Ld,ngBindHtml:Md,ngBindTemplate:Nd,ngClass:Od,ngClassEven:Pd,ngClassOdd:Qd,ngCloak:Rd,ngController:Sd,ngForm:Td,ngHide:Ud,ngIf:Vd,ngInclude:Wd,ngInit:Xd,ngNonBindable:Yd,ngPluralize:Zd,ngRepeat:$d,ngShow:ae,ngStyle:be,ngSwitch:ce,ngSwitchWhen:de,ngSwitchDefault:ee,ngOptions:fe,ngTransclude:ge,ngModel:he,ngList:ie,ngChange:je,pattern:lc,ngPattern:lc,required:mc,ngRequired:mc,minlength:nc,ngMinlength:nc,maxlength:oc,ngMaxlength:oc,ngValue:ke,
|
||||
ngModelOptions:le}).directive({ngInclude:me}).directive(kb).directive(pc);a.provider({$anchorScroll:ne,$animate:oe,$browser:pe,$cacheFactory:qe,$controller:re,$document:se,$exceptionHandler:te,$filter:qc,$interpolate:ue,$interval:ve,$http:we,$httpBackend:xe,$location:ye,$log:ze,$parse:Ae,$rootScope:Be,$q:Ce,$$q:De,$sce:Ee,$sceDelegate:Fe,$sniffer:Ge,$templateCache:He,$templateRequest:Ie,$$testability:Je,$timeout:Ke,$window:Le,$$rAF:Me,$$asyncCallback:Ne})}])}function Wa(b){return b.replace(Oe,function(a,
|
||||
b,d,e){return e?d.toUpperCase():d}).replace(Pe,"Moz$1")}function rc(b){b=b.nodeType;return 1===b||!b||9===b}function sc(b,a){var c,d,e=a.createDocumentFragment(),f=[];if(Gb.test(b)){c=c||e.appendChild(a.createElement("div"));d=(Qe.exec(b)||["",""])[1].toLowerCase();d=ia[d]||ia._default;c.innerHTML=d[1]+b.replace(Re,"<$1></$2>")+d[2];for(d=d[0];d--;)c=c.lastChild;f=db(f,c.childNodes);c=e.firstChild;c.textContent=""}else f.push(a.createTextNode(b));e.textContent="";e.innerHTML="";r(f,function(a){e.appendChild(a)});
|
||||
return e}function V(b){if(b instanceof V)return b;var a;C(b)&&(b=ba(b),a=!0);if(!(this instanceof V)){if(a&&"<"!=b.charAt(0))throw Hb("nosel");return new V(b)}if(a){a=Y;var c;b=(c=Se.exec(b))?[a.createElement(c[1])]:(c=sc(b,a))?c.childNodes:[]}tc(this,b)}function Ib(b){return b.cloneNode(!0)}function lb(b,a){a||mb(b);if(b.querySelectorAll)for(var c=b.querySelectorAll("*"),d=0,e=c.length;d<e;d++)mb(c[d])}function uc(b,a,c,d){if(B(d))throw Hb("offargs");var e=(d=nb(b))&&d.events;if(d&&d.handle)if(a)r(a.split(" "),
|
||||
function(a){F(c)?(b.removeEventListener(a,e[a],!1),delete e[a]):Ra(e[a]||[],c)});else for(a in e)"$destroy"!==a&&b.removeEventListener(a,e[a],!1),delete e[a]}function mb(b,a){var c=b.ng339,d=c&&ob[c];d&&(a?delete d.data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),uc(b)),delete ob[c],b.ng339=s))}function nb(b,a){var c=b.ng339,c=c&&ob[c];a&&!c&&(b.ng339=c=++Te,c=ob[c]={events:{},data:{},handle:s});return c}function Jb(b,a,c){if(rc(b)){var d=B(c),e=!d&&a&&!S(a),f=!a;b=(b=nb(b,!e))&&b.data;
|
||||
if(d)b[a]=c;else{if(f)return b;if(e)return b&&b[a];E(b,a)}}}function jb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function Kb(b,a){a&&b.setAttribute&&r(a.split(" "),function(a){b.setAttribute("class",ba((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+ba(a)+" "," ")))})}function Lb(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");r(a.split(" "),function(a){a=
|
||||
ba(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});b.setAttribute("class",ba(c))}}function tc(b,a){if(a)if(a.nodeType)b[b.length++]=a;else{var c=a.length;if("number"===typeof c&&a.window!==a){if(c)for(var d=0;d<c;d++)b[b.length++]=a[d]}else b[b.length++]=a}}function vc(b,a){return pb(b,"$"+(a||"ngController")+"Controller")}function pb(b,a,c){9==b.nodeType&&(b=b.documentElement);for(a=L(a)?a:[a];b;){for(var d=0,e=a.length;d<e;d++)if((c=G.data(b,a[d]))!==s)return c;b=b.parentNode||11===b.nodeType&&b.host}}
|
||||
function wc(b){for(lb(b,!0);b.firstChild;)b.removeChild(b.firstChild)}function xc(b,a){a||lb(b);var c=b.parentNode;c&&c.removeChild(b)}function yc(b,a){var c=qb[a.toLowerCase()];return c&&zc[pa(b)]&&c}function Ue(b,a){var c=b.nodeName;return("INPUT"===c||"TEXTAREA"===c)&&Ac[a]}function Ve(b,a){var c=function(c,e){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=a[e||c.type],g=f?f.length:0;if(g){1<g&&(f=qa(f));for(var h=0;h<g;h++)f[h].call(b,c)}};c.elem=b;return c}function Ka(b,a){var c=
|
||||
b&&b.$$hashKey;if(c)return"function"===typeof c&&(c=b.$$hashKey()),c;c=typeof b;return c="function"==c||"object"==c&&null!==b?b.$$hashKey=c+":"+(a||pd)():c+":"+b}function Xa(b,a){if(a){var c=0;this.nextUid=function(){return++c}}r(b,this.put,this)}function We(b){return(b=b.toString().replace(Bc,"").match(Cc))?"function("+(b[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function Mb(b,a,c){var d;if("function"===typeof b){if(!(d=b.$inject)){d=[];if(b.length){if(a)throw C(c)&&c||(c=b.name||We(b)),La("strictdi",
|
||||
c);a=b.toString().replace(Bc,"");a=a.match(Cc);r(a[1].split(Xe),function(a){a.replace(Ye,function(a,b,c){d.push(c)})})}b.$inject=d}}else L(b)?(a=b.length-1,gb(b[a],"fn"),d=b.slice(0,a)):gb(b,"fn",!0);return d}function Cb(b,a){function c(a){return function(b,c){if(S(b))r(b,$b(a));else return a(b,c)}}function d(a,b){Ja(a,"service");if(D(b)||L(b))b=p.instantiate(b);if(!b.$get)throw La("pget",a);return n[a+"Provider"]=b}function e(a,b){return d(a,{$get:b})}function f(a){var b=[],c;r(a,function(a){function d(a){var b,
|
||||
c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=p.get(e[0]);f[e[1]].apply(f,e[2])}}if(!k.get(a)){k.put(a,!0);try{C(a)?(c=Va(a),b=b.concat(f(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):D(a)?b.push(p.invoke(a)):L(a)?b.push(p.invoke(a)):gb(a,"module")}catch(e){throw L(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),La("modulerr",a,e.stack||e.message||e);}}});return b}function g(b,c){function d(a){if(b.hasOwnProperty(a)){if(b[a]===
|
||||
h)throw La("cdep",a+" <- "+m.join(" <- "));return b[a]}try{return m.unshift(a),b[a]=h,b[a]=c(a)}catch(e){throw b[a]===h&&delete b[a],e;}finally{m.shift()}}function e(b,c,f,h){"string"===typeof f&&(h=f,f=null);var g=[];h=Mb(b,a,h);var k,m,l;m=0;for(k=h.length;m<k;m++){l=h[m];if("string"!==typeof l)throw La("itkn",l);g.push(f&&f.hasOwnProperty(l)?f[l]:d(l))}L(b)&&(b=b[k]);return b.apply(c,g)}return{invoke:e,instantiate:function(a,b,c){var d=function(){};d.prototype=(L(a)?a[a.length-1]:a).prototype;
|
||||
d=new d;a=e(a,d,b,c);return S(a)||D(a)?a:d},get:d,annotate:Mb,has:function(a){return n.hasOwnProperty(a+"Provider")||b.hasOwnProperty(a)}}}a=!0===a;var h={},m=[],k=new Xa([],!0),n={$provide:{provider:c(d),factory:c(e),service:c(function(a,b){return e(a,["$injector",function(a){return a.instantiate(b)}])}),value:c(function(a,b){return e(a,da(b))}),constant:c(function(a,b){Ja(a,"constant");n[a]=b;l[a]=b}),decorator:function(a,b){var c=p.get(a+"Provider"),d=c.$get;c.$get=function(){var a=q.invoke(d,
|
||||
c);return q.invoke(b,null,{$delegate:a})}}}},p=n.$injector=g(n,function(){throw La("unpr",m.join(" <- "));}),l={},q=l.$injector=g(l,function(a){var b=p.get(a+"Provider");return q.invoke(b.$get,b,s,a)});r(f(b),function(a){q.invoke(a||w)});return q}function ne(){var b=!0;this.disableAutoScrolling=function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;r(a,function(a){b||"a"!==pa(a)||(b=a)});return b}function f(){var b=c.hash(),d;b?(d=g.getElementById(b))?
|
||||
d.scrollIntoView():(d=e(g.getElementsByName(b)))?d.scrollIntoView():"top"===b&&a.scrollTo(0,0):a.scrollTo(0,0)}var g=a.document;b&&d.$watch(function(){return c.hash()},function(){d.$evalAsync(f)});return f}]}function Ne(){this.$get=["$$rAF","$timeout",function(b,a){return b.supported?function(a){return b(a)}:function(b){return a(b,0,!1)}}]}function Ze(b,a,c,d){function e(a){try{a.apply(null,Ta.call(arguments,1))}finally{if(A--,0===A)for(;u.length;)try{u.pop()()}catch(b){c.error(b)}}}function f(a,
|
||||
b){(function R(){r(x,function(a){a()});z=b(R,a)})()}function g(){y=null;T!=h.url()&&(T=h.url(),r(Q,function(a){a(h.url())}))}var h=this,m=a[0],k=b.location,n=b.history,p=b.setTimeout,l=b.clearTimeout,q={};h.isMock=!1;var A=0,u=[];h.$$completeOutstandingRequest=e;h.$$incOutstandingRequestCount=function(){A++};h.notifyWhenNoOutstandingRequests=function(a){r(x,function(a){a()});0===A?a():u.push(a)};var x=[],z;h.addPollFn=function(a){F(z)&&f(100,p);x.push(a);return a};var T=k.href,v=a.find("base"),y=
|
||||
null;h.url=function(a,c){k!==b.location&&(k=b.location);n!==b.history&&(n=b.history);if(a){if(T!=a)return T=a,d.history?c?n.replaceState(null,"",a):(n.pushState(null,"",a),v.attr("href",v.attr("href"))):(y=a,c?k.replace(a):k.href=a),h}else return y||k.href.replace(/%27/g,"'")};var Q=[],ca=!1;h.onUrlChange=function(a){if(!ca){if(d.history)G(b).on("popstate",g);if(d.hashchange)G(b).on("hashchange",g);else h.addPollFn(g);ca=!0}Q.push(a);return a};h.$$checkUrlChange=g;h.baseHref=function(){var a=v.attr("href");
|
||||
return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};var J={},N="",M=h.baseHref();h.cookies=function(a,b){var d,e,f,h;if(a)b===s?m.cookie=encodeURIComponent(a)+"=;path="+M+";expires=Thu, 01 Jan 1970 00:00:00 GMT":C(b)&&(d=(m.cookie=encodeURIComponent(a)+"="+encodeURIComponent(b)+";path="+M).length+1,4096<d&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"));else{if(m.cookie!==N)for(N=m.cookie,d=N.split("; "),J={},f=0;f<d.length;f++)e=d[f],h=e.indexOf("="),
|
||||
0<h&&(a=decodeURIComponent(e.substring(0,h)),J[a]===s&&(J[a]=decodeURIComponent(e.substring(h+1))));return J}};h.defer=function(a,b){var c;A++;c=p(function(){delete q[c];e(a)},b||0);q[c]=!0;return c};h.defer.cancel=function(a){return q[a]?(delete q[a],l(a),e(w),!0):!1}}function pe(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new Ze(b,d,a,c)}]}function qe(){this.$get=function(){function b(b,d){function e(a){a!=p&&(l?l==a&&(l=a.n):l=a,f(a.n,a.p),f(a,p),p=a,p.n=null)}
|
||||
function f(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(b in a)throw K("$cacheFactory")("iid",b);var g=0,h=E({},d,{id:b}),m={},k=d&&d.capacity||Number.MAX_VALUE,n={},p=null,l=null;return a[b]={put:function(a,b){if(k<Number.MAX_VALUE){var c=n[a]||(n[a]={key:a});e(c)}if(!F(b))return a in m||g++,m[a]=b,g>k&&this.remove(l.key),b},get:function(a){if(k<Number.MAX_VALUE){var b=n[a];if(!b)return;e(b)}return m[a]},remove:function(a){if(k<Number.MAX_VALUE){var b=n[a];if(!b)return;b==p&&(p=b.p);b==l&&(l=b.n);f(b.n,
|
||||
b.p);delete n[a]}delete m[a];g--},removeAll:function(){m={};g=0;n={};p=l=null},destroy:function(){n=h=m=null;delete a[b]},info:function(){return E({},h,{size:g})}}}var a={};b.info=function(){var b={};r(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function He(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function jc(b,a){var c={},d=/^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/,e=/(([\d\w_\-]+)(?:\:([^;]+))?;?)/,f=rd("ngSrc,ngSrcset,src,srcset"),g=
|
||||
/^(on[a-z]+|formaction)$/;this.directive=function k(a,d){Ja(a,"directive");C(a)?(Fb(d,"directiveFactory"),c.hasOwnProperty(a)||(c[a]=[],b.factory(a+"Directive",["$injector","$exceptionHandler",function(b,d){var e=[];r(c[a],function(c,f){try{var h=b.invoke(c);D(h)?h={compile:da(h)}:!h.compile&&h.link&&(h.compile=da(h.link));h.priority=h.priority||0;h.index=f;h.name=h.name||a;h.require=h.require||h.controller&&h.name;h.restrict=h.restrict||"EA";e.push(h)}catch(g){d(g)}});return e}])),c[a].push(d)):
|
||||
r(a,$b(k));return this};this.aHrefSanitizationWhitelist=function(b){return B(b)?(a.aHrefSanitizationWhitelist(b),this):a.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(b){return B(b)?(a.imgSrcSanitizationWhitelist(b),this):a.imgSrcSanitizationWhitelist()};var h=!0;this.debugInfoEnabled=function(a){return B(a)?(h=a,this):h};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$document","$sce","$animate","$$sanitizeUri",
|
||||
function(a,b,p,l,q,A,u,x,z,T,v){function y(a,b){try{a.addClass(b)}catch(c){}}function Q(a,b,c,d,e){a instanceof G||(a=G(a));r(a,function(b,c){3==b.nodeType&&b.nodeValue.match(/\S+/)&&(a[c]=G(b).wrap("<span></span>").parent()[0])});var f=ca(a,b,a,c,d,e);Q.$$addScopeClass(a);var h=null,g=a,k;return function(b,c,d,e,l){Fb(b,"scope");h||(h=(l=l&&l[0])?"foreignobject"!==pa(l)&&l.toString().match(/SVG/)?"svg":"html":"html");"html"!==h&&a[0]!==k&&(g=G(Nb(h,G("<div>").append(a).html())));k=a[0];l=c?Ia.clone.call(g):
|
||||
g;if(d)for(var n in d)l.data("$"+n+"Controller",d[n].instance);Q.$$addScopeInfo(l,b);c&&c(l,b);f&&f(b,l,l,e);return l}}function ca(a,b,c,d,e,f){function h(a,c,d,e){var f,k,l,n,u,q,ua;if(p)for(ua=Array(c.length),n=0;n<g.length;n+=3)f=g[n],ua[f]=c[f];else ua=c;n=0;for(u=g.length;n<u;)k=ua[g[n++]],c=g[n++],f=g[n++],c?(c.scope?(l=a.$new(),Q.$$addScopeInfo(G(k),l)):l=a,q=c.transcludeOnThisElement?J(a,c.transclude,e,c.elementTranscludeOnThisElement):!c.templateOnThisElement&&e?e:!e&&b?J(a,b):null,c(f,l,
|
||||
k,d,q)):f&&f(a,k.childNodes,s,e)}for(var g=[],k,l,n,u,p,q=0;q<a.length;q++){k=new Ob;l=N(a[q],[],k,0===q?d:s,e);(f=l.length?H(l,a[q],k,b,c,null,[],[],f):null)&&f.scope&&Q.$$addScopeClass(k.$$element);k=f&&f.terminal||!(n=a[q].childNodes)||!n.length?null:ca(n,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||k)g.push(q,f,k),u=!0,p=p||f;f=null}return u?h:null}function J(a,b,c,d){return function(e,f,h,g){var k=!1;e||(e=a.$new(),k=e.$$transcluded=!0);f=b(e,f,h,c,g);if(k&&
|
||||
!d)f.on("$destroy",function(){e.$destroy()});return f}}function N(b,f,h,g,l){var n=h.$attr,u;switch(b.nodeType){case 1:R(f,va(pa(b)),"E",g,l);for(var p,q,T,A=b.attributes,z=0,v=A&&A.length;z<v;z++){var x=!1,J=!1;p=A[z];if(!X||8<=X||p.specified){u=p.name;p=ba(p.value);q=va(u);if(T=U.test(q))u=Db(q.substr(6),"-");var W=q.replace(/(Start|End)$/,""),r;a:{var H=W;if(c.hasOwnProperty(H)){r=void 0;for(var H=a.get(H+"Directive"),O=0,Q=H.length;O<Q;O++)if(r=H[O],r.multiElement){r=!0;break a}}r=!1}r&&q===W+
|
||||
"Start"&&(x=u,J=u.substr(0,u.length-5)+"end",u=u.substr(0,u.length-6));q=va(u.toLowerCase());n[q]=u;if(T||!h.hasOwnProperty(q))h[q]=p,yc(b,q)&&(h[q]=!0);ya(b,f,p,q,T);R(f,q,"A",g,l,x,J)}}b=b.className;if(C(b)&&""!==b)for(;u=e.exec(b);)q=va(u[2]),R(f,q,"C",g,l)&&(h[q]=ba(u[3])),b=b.substr(u.index+u[0].length);break;case 3:t(f,b.nodeValue);break;case 8:try{if(u=d.exec(b.nodeValue))q=va(u[1]),R(f,q,"M",g,l)&&(h[q]=ba(u[2]))}catch(M){}}f.sort(F);return f}function M(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&
|
||||
a.hasAttribute(b)){do{if(!a)throw ja("uterdir",b,c);1==a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return G(d)}function O(a,b,c){return function(d,e,f,h,g){e=M(e[0],b,c);return a(d,e,f,h,g)}}function H(a,c,d,e,f,h,g,k,l){function u(a,b,c,d){if(a){c&&(a=O(a,c,d));a.require=I.require;a.directiveName=ha;if(y===I||I.$$isolateScope)a=ga(a,{isolateScope:!0});g.push(a)}if(b){c&&(b=O(b,c,d));b.require=I.require;b.directiveName=ha;if(y===
|
||||
I||I.$$isolateScope)b=ga(b,{isolateScope:!0});k.push(b)}}function T(a,b,c,d){var e,f="data",h=!1;if(C(b)){for(;"^"==(e=b.charAt(0))||"?"==e;)b=b.substr(1),"^"==e&&(f="inheritedData"),h=h||"?"==e;e=null;d&&"data"===f&&(e=d[b])&&(e=e.instance);e=e||c[f]("$"+b+"Controller");if(!e&&!h)throw ja("ctreq",b,a);}else L(b)&&(e=[],r(b,function(b){e.push(T(a,b,c,d))}));return e}function z(a,e,f,h,l){function u(a,b,c){var d;Qa(a)||(c=b,b=a,a=s);E&&(d=W);c||(c=E?M.parent():M);return l(a,b,d,c)}var p,v,ua,x,W,O,
|
||||
M,R;c===f?(R=d,M=d.$$element):(M=G(f),R=new Ob(M,d));y&&(x=e.$new(!0));O=l&&u;J&&(H={},W={},r(J,function(a){var b={$scope:a===y||a.$$isolateScope?x:e,$element:M,$attrs:R,$transclude:O};ua=a.controller;"@"==ua&&(ua=R[a.name]);b=A(ua,b,!0,a.controllerAs);W[a.name]=b;E||M.data("$"+a.name+"Controller",b.instance);H[a.name]=b}));if(y){var N=/^\s*([@=&])(\??)\s*(\w*)\s*$/;Q.$$addScopeInfo(M,x,!0,!(ca&&(ca===y||ca===y.$$originalDirective)));Q.$$addScopeClass(M,!0);h=H&&H[y.name];var xa=x;h&&h.identifier&&
|
||||
!0===y.bindToController&&(xa=h.instance);r(y.scope,function(a,c){var d=a.match(N)||[],f=d[3]||c,h="?"==d[2],d=d[1],g,k,l,u;x.$$isolateBindings[c]=d+f;switch(d){case "@":R.$observe(f,function(a){x[c]=a});R.$$observers[f].$$scope=e;R[f]&&(xa[c]=b(R[f])(e));break;case "=":if(h&&!R[f])break;k=q(R[f]);u=k.literal?ra:function(a,b){return a===b||a!==a&&b!==b};l=k.assign||function(){g=xa[c]=k(e);throw ja("nonassign",R[f],y.name);};g=xa[c]=k(e);h=e.$watch(q(R[f],function(a){u(a,xa[c])||(u(a,g)?l(e,a=xa[c]):
|
||||
xa[c]=a);return g=a}),null,k.literal);x.$on("$destroy",h);break;case "&":k=q(R[f]);xa[c]=function(a){return k(e,a)};break;default:throw ja("iscp",y.name,c,a);}})}H&&(r(H,function(a){a()}),H=null);h=0;for(p=g.length;h<p;h++)v=g[h],Dc(v,v.isolateScope?x:e,M,R,v.require&&T(v.directiveName,v.require,M,W),O);h=e;y&&(y.template||null===y.templateUrl)&&(h=x);a&&a(h,f.childNodes,s,l);for(h=k.length-1;0<=h;h--)v=k[h],Dc(v,v.isolateScope?x:e,M,R,v.require&&T(v.directiveName,v.require,M,W),O)}l=l||{};for(var v=
|
||||
-Number.MAX_VALUE,x,J=l.controllerDirectives,H,y=l.newIsolateScopeDirective,ca=l.templateDirective,R=l.nonTlbTranscludeDirective,w=!1,F=!1,E=l.hasElementTranscludeDirective,aa=d.$$element=G(c),I,ha,t,P=e,za,ma=0,ya=a.length;ma<ya;ma++){I=a[ma];var U=I.$$start,X=I.$$end;U&&(aa=M(c,U,X));t=s;if(v>I.priority)break;if(t=I.scope)I.templateUrl||(S(t)?(K("new/isolated scope",y||x,I,aa),y=I):K("new/isolated scope",y,I,aa)),x=x||I;ha=I.name;!I.templateUrl&&I.controller&&(t=I.controller,J=J||{},K("'"+ha+"' controller",
|
||||
J[ha],I,aa),J[ha]=I);if(t=I.transclude)w=!0,I.$$tlb||(K("transclusion",R,I,aa),R=I),"element"==t?(E=!0,v=I.priority,t=aa,aa=d.$$element=G(Y.createComment(" "+ha+": "+d[ha]+" ")),c=aa[0],rb(f,Ta.call(t,0),c),P=Q(t,e,v,h&&h.name,{nonTlbTranscludeDirective:R})):(t=G(Ib(c)).contents(),aa.empty(),P=Q(t,e));if(I.template)if(F=!0,K("template",ca,I,aa),ca=I,t=D(I.template)?I.template(aa,d):I.template,t=V(t),I.replace){h=I;t=Gb.test(t)?G(Nb(I.templateNamespace,ba(t))):[];c=t[0];if(1!=t.length||1!==c.nodeType)throw ja("tplrt",
|
||||
ha,"");rb(f,aa,c);ya={$attr:{}};t=N(c,[],ya);var Z=a.splice(ma+1,a.length-(ma+1));y&&W(t);a=a.concat(t).concat(Z);B(d,ya);ya=a.length}else aa.html(t);if(I.templateUrl)F=!0,K("template",ca,I,aa),ca=I,I.replace&&(h=I),z=$e(a.splice(ma,a.length-ma),aa,d,f,w&&P,g,k,{controllerDirectives:J,newIsolateScopeDirective:y,templateDirective:ca,nonTlbTranscludeDirective:R}),ya=a.length;else if(I.compile)try{za=I.compile(aa,d,P),D(za)?u(null,za,U,X):za&&u(za.pre,za.post,U,X)}catch($){p($,ta(aa))}I.terminal&&(z.terminal=
|
||||
!0,v=Math.max(v,I.priority))}z.scope=x&&!0===x.scope;z.transcludeOnThisElement=w;z.elementTranscludeOnThisElement=E;z.templateOnThisElement=F;z.transclude=P;l.hasElementTranscludeDirective=E;return z}function W(a){for(var b=0,c=a.length;b<c;b++)a[b]=bc(a[b],{$$isolateScope:!0})}function R(b,d,e,f,h,g,l){if(d===h)return null;h=null;if(c.hasOwnProperty(d)){var n;d=a.get(d+"Directive");for(var u=0,q=d.length;u<q;u++)try{n=d[u],(f===s||f>n.priority)&&-1!=n.restrict.indexOf(e)&&(g&&(n=bc(n,{$$start:g,
|
||||
$$end:l})),b.push(n),h=n)}catch(T){p(T)}}return h}function B(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;r(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});r(b,function(b,f){"class"==f?(y(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function $e(a,b,c,d,e,f,h,g){var k=[],n,u,p=b[0],q=a.shift(),
|
||||
T=E({},q,{templateUrl:null,transclude:null,replace:null,$$originalDirective:q}),v=D(q.templateUrl)?q.templateUrl(b,c):q.templateUrl,A=q.templateNamespace;b.empty();l(z.getTrustedResourceUrl(v)).then(function(l){var z,x;l=V(l);if(q.replace){l=Gb.test(l)?G(Nb(A,ba(l))):[];z=l[0];if(1!=l.length||1!==z.nodeType)throw ja("tplrt",q.name,v);l={$attr:{}};rb(d,b,z);var O=N(z,[],l);S(q.scope)&&W(O);a=O.concat(a);B(c,l)}else z=p,b.html(l);a.unshift(T);n=H(a,z,c,e,b,q,f,h,g);r(d,function(a,c){a==z&&(d[c]=b[0])});
|
||||
for(u=ca(b[0].childNodes,e);k.length;){l=k.shift();x=k.shift();var M=k.shift(),Q=k.shift(),O=b[0];if(x!==p){var R=x.className;g.hasElementTranscludeDirective&&q.replace||(O=Ib(z));rb(M,G(x),O);y(G(O),R)}x=n.transcludeOnThisElement?J(l,n.transclude,Q):Q;n(u,l,O,d,x)}k=null});return function(a,b,c,d,e){a=e;k?(k.push(b),k.push(c),k.push(d),k.push(a)):(n.transcludeOnThisElement&&(a=J(b,n.transclude,e)),n(u,b,c,d,a))}}function F(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?
|
||||
-1:1:a.index-b.index}function K(a,b,c,d){if(b)throw ja("multidir",b.name,c.name,a,ta(d));}function t(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;b&&Q.$$addBindingClass(a);return function(a,c){var e=c.parent();b||Q.$$addBindingClass(e);Q.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function Nb(a,b){a=P(a||"html");switch(a){case "svg":case "math":var c=Y.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;
|
||||
default:return b}}function za(a,b){if("srcdoc"==b)return z.HTML;var c=pa(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&&("src"==b||"ngSrc"==b))return z.RESOURCE_URL}function ya(a,c,d,e,h){var k=b(d,!0);if(k){if("multiple"===e&&"select"===pa(a))throw ja("selmulti",ta(a));c.push({priority:100,compile:function(){return{pre:function(c,d,l){d=l.$$observers||(l.$$observers={});if(g.test(e))throw ja("nodomevents");if(k=b(l[e],!0,za(a,e),f[e]||h))l[e]=k(c),(d[e]||(d[e]=[])).$$inter=!0,(l.$$observers&&
|
||||
l.$$observers[e].$$scope||c).$watch(k,function(a,b){"class"===e&&a!=b?l.$updateClass(a,b):l.$set(e,a)})}}}})}}function rb(a,b,c){var d=b[0],e=b.length,f=d.parentNode,h,g;if(a)for(h=0,g=a.length;h<g;h++)if(a[h]==d){a[h++]=c;g=h+e-1;for(var k=a.length;h<k;h++,g++)g<k?a[h]=a[g]:delete a[h];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=Y.createDocumentFragment();a.appendChild(d);G(c).data(G(d).data());la?(Eb=!0,la.cleanData([d])):delete G.cache[d[G.expando]];d=1;for(e=b.length;d<
|
||||
e;d++)f=b[d],G(f).remove(),a.appendChild(f),delete b[d];b[0]=c;b.length=1}function ga(a,b){return E(function(){return a.apply(null,arguments)},a,b)}function Dc(a,b,c,d,e,f){try{a(b,c,d,e,f)}catch(h){p(h,ta(c))}}var Ob=function(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a};Ob.prototype={$normalize:va,$addClass:function(a){a&&0<a.length&&T.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&T.removeClass(this.$$element,
|
||||
a)},$updateClass:function(a,b){var c=Ec(a,b);c&&c.length&&T.addClass(this.$$element,c);(c=Ec(b,a))&&c.length&&T.removeClass(this.$$element,c)},$set:function(a,b,c,d){var e=this.$$element[0],f=yc(e,a),h=Ue(e,a),e=a;f?(this.$$element.prop(a,b),d=f):h&&(this[h]=b,e=h);this[a]=b;d?this.$attr[a]=d:(d=this.$attr[a])||(this.$attr[a]=d=Db(a,"-"));f=pa(this.$$element);if("a"===f&&"href"===a||"img"===f&&"src"===a)this[a]=b=v(b,"src"===a);!1!==c&&(null===b||b===s?this.$$element.removeAttr(d):this.$$element.attr(d,
|
||||
b));(a=this.$$observers)&&r(a[e],function(a){try{a(b)}catch(c){p(c)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers={}),e=d[a]||(d[a]=[]);e.push(b);u.$evalAsync(function(){e.$$inter||b(c[a])});return function(){Ra(e,b)}}};var ma=b.startSymbol(),ha=b.endSymbol(),V="{{"==ma||"}}"==ha?Pa:function(a){return a.replace(/\{\{/g,ma).replace(/}}/g,ha)},U=/^ngAttr[A-Z]/;Q.$$addBindingInfo=h?function(a,b){var c=a.data("$binding")||[];L(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:w;
|
||||
Q.$$addBindingClass=h?function(a){y(a,"ng-binding")}:w;Q.$$addScopeInfo=h?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:w;Q.$$addScopeClass=h?function(a,b){y(a,b?"ng-isolate-scope":"ng-scope")}:w;return Q}]}function va(b){return Wa(b.replace(af,""))}function Ec(b,a){var c="",d=b.split(/\s+/),e=a.split(/\s+/),f=0;a:for(;f<d.length;f++){for(var g=d[f],h=0;h<e.length;h++)if(g==e[h])continue a;c+=(0<c.length?" ":"")+g}return c}function re(){var b={},a=!1,c=/^(\S+)(\s+as\s+(\w+))?$/;
|
||||
this.register=function(a,c){Ja(a,"controller");S(a)?E(b,a):b[a]=c};this.allowGlobals=function(){a=!0};this.$get=["$injector","$window",function(d,e){function f(a,b,c,d){if(!a||!S(a.$scope))throw K("$controller")("noscp",d,b);a.$scope[b]=c}return function(g,h,m,k){var n,p,l;m=!0===m;k&&C(k)&&(l=k);C(g)&&(k=g.match(c),p=k[1],l=l||k[3],g=b.hasOwnProperty(p)?b[p]:ic(h.$scope,p,!0)||(a?ic(e,p,!0):s),gb(g,p,!0));if(m)return m=function(){},m.prototype=(L(g)?g[g.length-1]:g).prototype,n=new m,l&&f(h,l,n,
|
||||
p||g.name),E(function(){d.invoke(g,n,h,p);return n},{instance:n,identifier:l});n=d.instantiate(g,h,p);l&&f(h,l,n,p||g.name);return n}}]}function se(){this.$get=["$window",function(b){return G(b.document)}]}function te(){this.$get=["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function Fc(b){var a={},c,d,e;if(!b)return a;r(b.split("\n"),function(b){e=b.indexOf(":");c=P(ba(b.substr(0,e)));d=ba(b.substr(e+1));c&&(a[c]=a[c]?a[c]+", "+d:d)});return a}function Gc(b){var a=S(b)?b:
|
||||
s;return function(c){a||(a=Fc(b));return c?a[P(c)]||null:a}}function Hc(b,a,c){if(D(c))return c(b,a);r(c,function(c){b=c(b,a)});return b}function we(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d={"Content-Type":"application/json;charset=utf-8"},e=this.defaults={transformResponse:[function(d){C(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=dc(d)));return d}],transformRequest:[function(a){return S(a)&&"[object File]"!==Ga.call(a)&&"[object Blob]"!==Ga.call(a)?sa(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},
|
||||
post:qa(d),put:qa(d),patch:qa(d)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"},f=!1;this.useApplyAsync=function(a){return B(a)?(f=!!a,this):f};var g=this.interceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,d,p,l){function q(a){function b(a){var d=E({},a,{data:Hc(a.data,a.headers,c.transformResponse)});a=a.status;return 200<=a&&300>a?d:p.reject(d)}var c={method:"get",transformRequest:e.transformRequest,transformResponse:e.transformResponse},
|
||||
d=function(a){var b=e.headers,c=E({},a.headers),d,f,b=E({},b.common,b[P(a.method)]);a:for(d in b){a=P(d);for(f in c)if(P(f)===a)continue a;c[d]=b[d]}(function(a){var b;r(a,function(c,d){D(c)&&(b=c(),null!=b?a[d]=b:delete a[d])})})(c);return c}(a);E(c,a);c.headers=d;c.method=ib(c.method);var f=[function(a){d=a.headers;var c=Hc(a.data,Gc(d),a.transformRequest);F(c)&&r(d,function(a,b){"content-type"===P(b)&&delete d[b]});F(a.withCredentials)&&!F(e.withCredentials)&&(a.withCredentials=e.withCredentials);
|
||||
return A(a,c,d).then(b,b)},s],h=p.when(c);for(r(z,function(a){(a.request||a.requestError)&&f.unshift(a.request,a.requestError);(a.response||a.responseError)&&f.push(a.response,a.responseError)});f.length;){a=f.shift();var g=f.shift(),h=h.then(a,g)}h.success=function(a){h.then(function(b){a(b.data,b.status,b.headers,c)});return h};h.error=function(a){h.then(null,function(b){a(b.data,b.status,b.headers,c)});return h};return h}function A(c,g,k){function l(a,b,c,e){function h(){z(b,a,c,e)}O&&(200<=a&&
|
||||
300>a?O.put(W,[a,b,Fc(c),e]):O.remove(W));f?d.$applyAsync(h):(h(),d.$$phase||d.$apply())}function z(a,b,d,e){b=Math.max(b,0);(200<=b&&300>b?r.resolve:r.reject)({data:a,status:b,headers:Gc(d),config:c,statusText:e})}function A(){var a=q.pendingRequests.indexOf(c);-1!==a&&q.pendingRequests.splice(a,1)}var r=p.defer(),M=r.promise,O,H,W=u(c.url,c.params);q.pendingRequests.push(c);M.then(A,A);!c.cache&&!e.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(O=S(c.cache)?c.cache:S(e.cache)?e.cache:
|
||||
x);if(O)if(H=O.get(W),B(H)){if(H&&D(H.then))return H.then(A,A),H;L(H)?z(H[1],H[0],qa(H[2]),H[3]):z(H,200,{},"OK")}else O.put(W,M);F(H)&&((H=Ic(c.url)?b.cookies()[c.xsrfCookieName||e.xsrfCookieName]:s)&&(k[c.xsrfHeaderName||e.xsrfHeaderName]=H),a(c.method,W,g,l,k,c.timeout,c.withCredentials,c.responseType));return M}function u(a,b){if(!b)return a;var c=[];od(b,function(a,b){null===a||F(a)||(L(a)||(a=[a]),r(a,function(a){S(a)&&(a=fa(a)?a.toISOString():sa(a));c.push(Da(b)+"="+Da(a))}))});0<c.length&&
|
||||
(a+=(-1==a.indexOf("?")?"?":"&")+c.join("&"));return a}var x=c("$http"),z=[];r(g,function(a){z.unshift(C(a)?l.get(a):l.invoke(a))});q.pendingRequests=[];(function(a){r(arguments,function(a){q[a]=function(b,c){return q(E(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){r(arguments,function(a){q[a]=function(b,c,d){return q(E(d||{},{method:a,url:b,data:c}))}})})("post","put","patch");q.defaults=e;return q}]}function bf(b){if(8>=X&&(!b.match(/^(get|post|head|put|delete|options)$/i)||
|
||||
!t.XMLHttpRequest))return new t.ActiveXObject("Microsoft.XMLHTTP");if(t.XMLHttpRequest)return new t.XMLHttpRequest;throw K("$httpBackend")("noxhr");}function xe(){this.$get=["$browser","$window","$document",function(b,a,c){return cf(b,bf,b.defer,a.angular.callbacks,c[0])}]}function cf(b,a,c,d,e){function f(a,b,c){var f=e.createElement("script"),n=null;f.type="text/javascript";f.src=a;f.async=!0;n=function(a){f.removeEventListener("load",n,!1);f.removeEventListener("error",n,!1);e.body.removeChild(f);
|
||||
f=null;var g=-1,q="unknown";a&&("load"!==a.type||d[b].called||(a={type:"error"}),q=a.type,g="error"===a.type?404:200);c&&c(g,q)};f.addEventListener("load",n,!1);f.addEventListener("error",n,!1);e.body.appendChild(f);return n}return function(e,h,m,k,n,p,l,q){function A(){x=-1;T&&T();v&&v.abort()}function u(a,d,e,f,g){Q&&c.cancel(Q);T=v=null;0===d&&(d=e?200:"file"==Aa(h).protocol?404:0);a(1223===d?204:d,e,f,g||"");b.$$completeOutstandingRequest(w)}var x;b.$$incOutstandingRequestCount();h=h||b.url();
|
||||
if("jsonp"==P(e)){var z="_"+(d.counter++).toString(36);d[z]=function(a){d[z].data=a;d[z].called=!0};var T=f(h.replace("JSON_CALLBACK","angular.callbacks."+z),z,function(a,b){u(k,a,d[z].data,"",b);d[z]=w})}else{var v=a(e);v.open(e,h,!0);r(n,function(a,b){B(a)&&v.setRequestHeader(b,a)});v.onreadystatechange=function(){if(v&&4==v.readyState){var a=null,b=null,c="";-1!==x&&(a=v.getAllResponseHeaders(),b="response"in v?v.response:v.responseText);-1===x&&10>X||(c=v.statusText);u(k,x||v.status,b,a,c)}};
|
||||
l&&(v.withCredentials=!0);if(q)try{v.responseType=q}catch(y){if("json"!==q)throw y;}v.send(m||null)}if(0<p)var Q=c(A,p);else p&&D(p.then)&&p.then(A)}}function ue(){var b="{{",a="}}";this.startSymbol=function(a){return a?(b=a,this):b};this.endSymbol=function(b){return b?(a=b,this):a};this.$get=["$parse","$exceptionHandler","$sce",function(c,d,e){function f(a){return"\\\\\\"+a}function g(f,g,q,A){function u(c){return c.replace(k,b).replace(n,a)}function x(a){try{var b;var c=q?e.getTrusted(q,a):e.valueOf(a);
|
||||
if(null==c)b="";else{switch(typeof c){case "string":break;case "number":c=""+c;break;default:c=sa(c)}b=c}return b}catch(h){a=Pb("interr",f,h.toString()),d(a)}}A=!!A;for(var z,T,v=0,r=[],Q=[],s=f.length,J=[],N=[];v<s;)if(-1!=(z=f.indexOf(b,v))&&-1!=(T=f.indexOf(a,z+h)))v!==z&&J.push(u(f.substring(v,z))),v=f.substring(z+h,T),r.push(v),Q.push(c(v,x)),v=T+m,N.push(J.length),J.push("");else{v!==s&&J.push(u(f.substring(v)));break}if(q&&1<J.length)throw Pb("noconcat",f);if(!g||r.length){var M=function(a){for(var b=
|
||||
0,c=r.length;b<c;b++){if(A&&F(a[b]))return;J[N[b]]=a[b]}return J.join("")};return E(function(a){var b=0,c=r.length,e=Array(c);try{for(;b<c;b++)e[b]=Q[b](a);return M(e)}catch(h){a=Pb("interr",f,h.toString()),d(a)}},{exp:f,expressions:r,$$watchDelegate:function(a,b,c){var d;return a.$watchGroup(Q,function(c,e){var f=M(c);D(b)&&b.call(this,f,c!==e?d:f,a);d=f},c)}})}}var h=b.length,m=a.length,k=new RegExp(b.replace(/./g,f),"g"),n=new RegExp(a.replace(/./g,f),"g");g.startSymbol=function(){return b};g.endSymbol=
|
||||
function(){return a};return g}]}function ve(){this.$get=["$rootScope","$window","$q","$$q",function(b,a,c,d){function e(e,h,m,k){var n=a.setInterval,p=a.clearInterval,l=0,q=B(k)&&!k,A=(q?d:c).defer(),u=A.promise;m=B(m)?m:0;u.then(null,null,e);u.$$intervalId=n(function(){A.notify(l++);0<m&&l>=m&&(A.resolve(l),p(u.$$intervalId),delete f[u.$$intervalId]);q||b.$apply()},h);f[u.$$intervalId]=A;return u}var f={};e.cancel=function(b){return b&&b.$$intervalId in f?(f[b.$$intervalId].reject("canceled"),a.clearInterval(b.$$intervalId),
|
||||
delete f[b.$$intervalId],!0):!1};return e}]}function Dd(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January February March April May June July August September October November December".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),
|
||||
DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a",short:"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return 1===b?"one":"other"}}}}function Qb(b){b=b.split("/");for(var a=b.length;a--;)b[a]=eb(b[a]);return b.join("/")}function Jc(b,a,c){b=Aa(b,c);a.$$protocol=
|
||||
b.protocol;a.$$host=b.hostname;a.$$port=U(b.port)||df[b.protocol]||null}function Kc(b,a,c){var d="/"!==b.charAt(0);d&&(b="/"+b);b=Aa(b,c);a.$$path=decodeURIComponent(d&&"/"===b.pathname.charAt(0)?b.pathname.substring(1):b.pathname);a.$$search=fc(b.search);a.$$hash=decodeURIComponent(b.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function wa(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Ya(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Rb(b){return b.substr(0,
|
||||
Ya(b).lastIndexOf("/")+1)}function Lc(b,a){this.$$html5=!0;a=a||"";var c=Rb(b);Jc(b,this,b);this.$$parse=function(a){var e=wa(c,a);if(!C(e))throw sb("ipthprfx",a,c);Kc(e,this,b);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Bb(this.$$search),b=this.$$hash?"#"+eb(this.$$hash):"";this.$$url=Qb(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;(f=wa(b,d))!==s?
|
||||
(g=f,g=(f=wa(a,f))!==s?c+(wa("/",f)||f):b+g):(f=wa(c,d))!==s?g=c+f:c==d+"/"&&(g=c);g&&this.$$parse(g);return!!g}}function Sb(b,a){var c=Rb(b);Jc(b,this,b);this.$$parse=function(d){var e=wa(b,d)||wa(c,d),e="#"==e.charAt(0)?wa(a,e):this.$$html5?e:"";if(!C(e))throw sb("ihshprfx",d,a);Kc(e,this,b);d=this.$$path;var f=/^\/[A-Z]:(\/.*)/;0===e.indexOf(b)&&(e=e.replace(b,""));f.exec(e)||(d=(e=f.exec(d))?e[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var c=Bb(this.$$search),e=this.$$hash?
|
||||
"#"+eb(this.$$hash):"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$parseLinkUrl=function(a,c){return Ya(b)==Ya(a)?(this.$$parse(a),!0):!1}}function Mc(b,a){this.$$html5=!0;Sb.apply(this,arguments);var c=Rb(b);this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;b==Ya(d)?f=d:(g=wa(c,d))?f=b+a+g:c===d+"/"&&(f=c);f&&this.$$parse(f);return!!f};this.$$compose=function(){var c=Bb(this.$$search),e=this.$$hash?"#"+eb(this.$$hash):
|
||||
"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function tb(b){return function(){return this[b]}}function Nc(b,a){return function(c){if(F(c))return this[b];this[b]=a(c);this.$$compose();return this}}function ye(){var b="",a=!1;this.hashPrefix=function(a){return B(a)?(b=a,this):b};this.html5Mode=function(b){return B(b)?(a=b,this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement",function(c,d,e,f){function g(a){c.$broadcast("$locationChangeSuccess",h.absUrl(),
|
||||
a)}var h,m=d.baseHref(),k=d.url();if(a){if(!m)throw sb("nobase");m=k.substring(0,k.indexOf("/",k.indexOf("//")+2))+(m||"/");e=e.history?Lc:Mc}else m=Ya(k),e=Sb;h=new e(m,"#"+b);h.$$parseLinkUrl(k,k);var n=/^\s*(javascript|mailto):/i;f.on("click",function(a){if(!a.ctrlKey&&!a.metaKey&&2!=a.which){for(var b=G(a.target);"a"!==pa(b[0]);)if(b[0]===f[0]||!(b=b.parent())[0])return;var e=b.prop("href"),g=b.attr("href")||b.attr("xlink:href");S(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=Aa(e.animVal).href);
|
||||
n.test(e)||!e||b.attr("target")||a.isDefaultPrevented()||!h.$$parseLinkUrl(e,g)||(a.preventDefault(),h.absUrl()!=d.url()&&(c.$apply(),t.angular["ff-684208-preventDefault"]=!0))}});h.absUrl()!=k&&d.url(h.absUrl(),!0);d.onUrlChange(function(a){h.absUrl()!=a&&(c.$evalAsync(function(){var b=h.absUrl();h.$$parse(a);c.$broadcast("$locationChangeStart",a,b).defaultPrevented?(h.$$parse(b),d.url(b)):g(b)}),c.$$phase||c.$digest())});var p=0;c.$watch(function(){var a=d.url(),b=h.$$replace;p&&a==h.absUrl()||
|
||||
(p++,c.$evalAsync(function(){c.$broadcast("$locationChangeStart",h.absUrl(),a).defaultPrevented?h.$$parse(a):(d.url(h.absUrl(),b),g(a))}));h.$$replace=!1;return p});return h}]}function ze(){var b=!0,a=this;this.debugEnabled=function(a){return B(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=
|
||||
c.console||{},e=b[a]||b.log||w;a=!1;try{a=!!e.apply}catch(m){}return a?function(){var a=[];r(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function na(b,a){if("__defineGetter__"===b||"__defineSetter__"===b||"__lookupGetter__"===b||"__lookupSetter__"===b||"__proto__"===b)throw oa("isecfld",a);return b}function Ba(b,
|
||||
a){if(b){if(b.constructor===b)throw oa("isecfn",a);if(b.window===b)throw oa("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw oa("isecdom",a);if(b===Object)throw oa("isecobj",a);}return b}function ub(b,a,c,d){Ba(b,d);a=a.split(".");for(var e,f=0;1<a.length;f++){e=na(a.shift(),d);var g=Ba(b[e],d);g||(g={},b[e]=g);b=g}e=na(a.shift(),d);Ba(b[e],d);return b[e]=c}function Oc(b,a,c,d,e,f){na(b,f);na(a,f);na(c,f);na(d,f);na(e,f);return function(f,h){var m=h&&h.hasOwnProperty(b)?h:
|
||||
f;if(null==m)return m;m=m[b];if(!a)return m;if(null==m)return s;m=m[a];if(!c)return m;if(null==m)return s;m=m[c];if(!d)return m;if(null==m)return s;m=m[d];return e?null==m?s:m=m[e]:m}}function Pc(b,a,c){var d=Qc[b];if(d)return d;var e=b.split("."),f=e.length;if(a.csp)d=6>f?Oc(e[0],e[1],e[2],e[3],e[4],c):function(a,b){var d=0,g;do g=Oc(e[d++],e[d++],e[d++],e[d++],e[d++],c)(a,b),b=s,a=g;while(d<f);return g};else{var g="";r(e,function(a,b){na(a,c);g+="if(s == null) return undefined;\ns="+(b?"s":'((l&&l.hasOwnProperty("'+
|
||||
a+'"))?l:s)')+"."+a+";\n"});g+="return s;";a=new Function("s","l",g);a.toString=da(g);a.assign=function(a,c){return ub(a,b,c,b)};d=a}d.sharedGetter=!0;return Qc[b]=d}function Ae(){var b=Object.create(null),a={csp:!1};this.$get=["$filter","$sniffer",function(c,d){function e(a){var b=a;a.sharedGetter&&(b=function(b,c){return a(b,c)},b.literal=a.literal,b.constant=a.constant,b.assign=a.assign);return b}function f(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;D(b)&&b.apply(this,
|
||||
arguments);B(a)&&d.$$postDigest(function(){B(f)&&e()})},c)}function g(a,b,c,d){function e(a){var b=!0;r(a,function(a){B(a)||(b=!1)});return b}var f;return f=a.$watch(function(a){return d(a)},function(a,c,d){D(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(a)&&f()})},c)}function h(a,b,c,d){var e;return e=a.$watch(function(a){return d(a)},function(a,c,d){D(b)&&b.apply(this,arguments);e()},c)}function m(a,b){if(!b)return a;var c=function(c,d){var e=a(c,d),f=b(e,c,d);return B(e)?f:e};c.$$watchDelegate=
|
||||
a.$$watchDelegate;return c}a.csp=d.csp;return function(d,n){var p,l,q;switch(typeof d){case "string":return q=d=d.trim(),p=b[q],p||(":"===d.charAt(0)&&":"===d.charAt(1)&&(l=!0,d=d.substring(2)),p=new Tb(a),p=(new Za(p,c,a)).parse(d),p.constant?p.$$watchDelegate=h:l&&(p=e(p),p.$$watchDelegate=p.literal?g:f),b[q]=p),m(p,n);case "function":return m(d,n);default:return m(w,n)}}}]}function Ce(){this.$get=["$rootScope","$exceptionHandler",function(b,a){return Rc(function(a){b.$evalAsync(a)},a)}]}function De(){this.$get=
|
||||
["$browser","$exceptionHandler",function(b,a){return Rc(function(a){b.defer(a)},a)}]}function Rc(b,a){function c(a,b,c){function d(b){return function(c){e||(e=!0,b.call(a,c))}}var e=!1;return[d(b),d(c)]}function d(){this.$$state={status:0}}function e(a,b){return function(c){b.call(a,c)}}function f(c){!c.processScheduled&&c.pending&&(c.processScheduled=!0,b(function(){var b,d,e;e=c.pending;c.processScheduled=!1;c.pending=s;for(var f=0,h=e.length;f<h;++f){d=e[f][0];b=e[f][c.status];try{D(b)?d.resolve(b(c.value)):
|
||||
1===c.status?d.resolve(c.value):d.reject(c.value)}catch(g){d.reject(g),a(g)}}}))}function g(){this.promise=new d;this.resolve=e(this,this.resolve);this.reject=e(this,this.reject);this.notify=e(this,this.notify)}var h=K("$q",TypeError);d.prototype={then:function(a,b,c){var d=new g;this.$$state.pending=this.$$state.pending||[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&f(this.$$state);return d.promise},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return k(b,
|
||||
!0,a)},function(b){return k(b,!1,a)},b)}};g.prototype={resolve:function(a){this.promise.$$state.status||(a===this.promise?this.$$reject(h("qcycle",a)):this.$$resolve(a))},$$resolve:function(b){var d,e;e=c(this,this.$$resolve,this.$$reject);try{if(S(b)||D(b))d=b&&b.then;D(d)?(this.promise.$$state.status=-1,d.call(b,e[0],e[1],this.notify)):(this.promise.$$state.value=b,this.promise.$$state.status=1,f(this.promise.$$state))}catch(h){e[1](h),a(h)}},reject:function(a){this.promise.$$state.status||this.$$reject(a)},
|
||||
$$reject:function(a){this.promise.$$state.value=a;this.promise.$$state.status=2;f(this.promise.$$state)},notify:function(c){var d=this.promise.$$state.pending;0>=this.promise.$$state.status&&d&&d.length&&b(function(){for(var b,e,f=0,h=d.length;f<h;f++){e=d[f][0];b=d[f][3];try{e.notify(D(b)?b(c):c)}catch(g){a(g)}}})}};var m=function(a,b){var c=new g;b?c.resolve(a):c.reject(a);return c.promise},k=function(a,b,c){var d=null;try{D(c)&&(d=c())}catch(e){return m(e,!1)}return d&&D(d.then)?d.then(function(){return m(a,
|
||||
b)},function(a){return m(a,!1)}):m(a,b)},n=function(a,b,c,d){var e=new g;e.resolve(a);return e.promise.then(b,c,d)},p=function q(a){if(!D(a))throw h("norslvr",a);if(!(this instanceof q))return new q(a);var b=new g;a(function(a){b.resolve(a)},function(a){b.reject(a)});return b.promise};p.defer=function(){return new g};p.reject=function(a){var b=new g;b.reject(a);return b.promise};p.when=n;p.all=function(a){var b=new g,c=0,d=L(a)?[]:{};r(a,function(a,e){c++;n(a).then(function(a){d.hasOwnProperty(e)||
|
||||
(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise};return p}function Me(){this.$get=["$window","$timeout",function(b,a){var c=b.requestAnimationFrame||b.webkitRequestAnimationFrame||b.mozRequestAnimationFrame,d=b.cancelAnimationFrame||b.webkitCancelAnimationFrame||b.mozCancelAnimationFrame||b.webkitCancelRequestAnimationFrame,e=!!c,f=e?function(a){var b=c(a);return function(){d(b)}}:function(b){var c=a(b,16.66,!1);return function(){a.cancel(c)}};
|
||||
f.supported=e;return f}]}function Be(){var b=10,a=K("$rootScope"),c=null,d=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(e,f,g,h){function m(){this.$id=++bb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this["this"]=this.$root=this;this.$$destroyed=!1;this.$$asyncQueue=[];this.$$postDigestQueue=[];this.$$listeners={};this.$$listenerCount=
|
||||
{};this.$$isolateBindings={};this.$$applyAsyncQueue=[]}function k(b){if(A.$$phase)throw a("inprog",A.$$phase);A.$$phase=b}function n(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function p(){}function l(){for(var a=A.$$applyAsyncQueue;a.length;)try{a.shift()()}catch(b){f(b)}d=null}function q(){null===d&&(d=h.defer(function(){A.$apply(l)}))}m.prototype={constructor:m,$new:function(a){a?(a=new m,a.$root=this.$root,a.$$asyncQueue=this.$$asyncQueue,
|
||||
a.$$postDigestQueue=this.$$postDigestQueue):(this.$$ChildScope||(this.$$ChildScope=function(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$id=++bb;this.$$ChildScope=null},this.$$ChildScope.prototype=this),a=new this.$$ChildScope);a["this"]=a;a.$parent=this;a.$$prevSibling=this.$$childTail;this.$$childHead?this.$$childTail=this.$$childTail.$$nextSibling=a:this.$$childHead=this.$$childTail=a;return a},$watch:function(a,b,
|
||||
d){var e=g(a);if(e.$$watchDelegate)return e.$$watchDelegate(this,b,d,e);var f=this.$$watchers,h={fn:b,last:p,get:e,exp:a,eq:!!d};c=null;D(b)||(h.fn=w);f||(f=this.$$watchers=[]);f.unshift(h);return function(){Ra(f,h);c=null}},$watchGroup:function(a,b){function c(){g=!1;k?(k=!1,b(e,e,h)):b(e,d,h)}var d=Array(a.length),e=Array(a.length),f=[],h=this,g=!1,k=!0;if(!a.length){var m=!0;h.$evalAsync(function(){m&&b(e,e,h)});return function(){m=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=
|
||||
a;d[0]=c;b(e,a===c?e:d,f)});r(a,function(a,b){var k=h.$watch(a,function(a,f){e[b]=a;d[b]=f;g||(g=!0,h.$evalAsync(c))});f.push(k)});return function(){for(;f.length;)f.shift()()}},$watchCollection:function(a,b){var c=this,d,e,f,h=1<b.length,k=0,m=g(a,function(a){d=a;var b,c,f,h;if(S(d))if(Na(d))for(e!==l&&(e=l,q=e.length=0,k++),a=d.length,q!==a&&(k++,e.length=q=a),b=0;b<a;b++)h=e[b],f=d[b],c=h!==h&&f!==f,c||h===f||(k++,e[b]=f);else{e!==n&&(e=n={},q=0,k++);a=0;for(b in d)d.hasOwnProperty(b)&&(a++,f=
|
||||
d[b],h=e[b],b in e?(c=h!==h&&f!==f,c||h===f||(k++,e[b]=f)):(q++,e[b]=f,k++));if(q>a)for(b in k++,e)d.hasOwnProperty(b)||(q--,delete e[b])}else e!==d&&(e=d,k++);return k}),l=[],n={},p=!0,q=0;return this.$watch(m,function(){p?(p=!1,b(d,d,c)):b(d,f,c);if(h)if(S(d))if(Na(d)){f=Array(d.length);for(var a=0;a<d.length;a++)f[a]=d[a]}else for(a in f={},d)Ab.call(d,a)&&(f[a]=d[a]);else f=d})},$digest:function(){var e,g,m,n,q=this.$$asyncQueue,r=this.$$postDigestQueue,s,B,J=b,N,M=[],O,H,W;k("$digest");h.$$checkUrlChange();
|
||||
this===A&&null!==d&&(h.defer.cancel(d),l());c=null;do{B=!1;for(N=this;q.length;){try{W=q.shift(),W.scope.$eval(W.expression)}catch(R){f(R)}c=null}a:do{if(n=N.$$watchers)for(s=n.length;s--;)try{if(e=n[s])if((g=e.get(N))!==(m=e.last)&&!(e.eq?ra(g,m):"number"===typeof g&&"number"===typeof m&&isNaN(g)&&isNaN(m)))B=!0,c=e,e.last=e.eq?Ha(g,null):g,e.fn(g,m===p?g:m,N),5>J&&(O=4-J,M[O]||(M[O]=[]),H=D(e.exp)?"fn: "+(e.exp.name||e.exp.toString()):e.exp,H+="; newVal: "+sa(g)+"; oldVal: "+sa(m),M[O].push(H));
|
||||
else if(e===c){B=!1;break a}}catch(t){f(t)}if(!(n=N.$$childHead||N!==this&&N.$$nextSibling))for(;N!==this&&!(n=N.$$nextSibling);)N=N.$parent}while(N=n);if((B||q.length)&&!J--)throw A.$$phase=null,a("infdig",b,sa(M));}while(B||q.length);for(A.$$phase=null;r.length;)try{r.shift()()}catch(G){f(G)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;if(this!==A){for(var b in this.$$listenerCount)n(this,this.$$listenerCount[b],b);a.$$childHead==
|
||||
this&&(a.$$childHead=this.$$nextSibling);a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=null;this.$$listeners={};this.$$watchers=this.$$asyncQueue=this.$$postDigestQueue=[];this.$destroy=this.$digest=this.$apply=w;this.$on=this.$watch=this.$watchGroup=
|
||||
function(){return w}}}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a){A.$$phase||A.$$asyncQueue.length||h.defer(function(){A.$$asyncQueue.length&&A.$digest()});this.$$asyncQueue.push({scope:this,expression:a})},$$postDigest:function(a){this.$$postDigestQueue.push(a)},$apply:function(a){try{return k("$apply"),this.$eval(a)}catch(b){f(b)}finally{A.$$phase=null;try{A.$digest()}catch(c){throw f(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&A.$$applyAsyncQueue.push(b);
|
||||
q()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){c[c.indexOf(b)]=null;n(e,1,a)}},$emit:function(a,b){var c=[],d,e=this,h=!1,g={name:a,targetScope:e,stopPropagation:function(){h=!0},preventDefault:function(){g.defaultPrevented=!0},defaultPrevented:!1},k=db([g],arguments,1),m,l;do{d=e.$$listeners[a]||c;g.currentScope=e;m=0;for(l=
|
||||
d.length;m<l;m++)if(d[m])try{d[m].apply(null,k)}catch(n){f(n)}else d.splice(m,1),m--,l--;if(h)return g.currentScope=null,g;e=e.$parent}while(e);g.currentScope=null;return g},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var h=db([e],arguments,1),g,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];g=0;for(k=d.length;g<k;g++)if(d[g])try{d[g].apply(null,h)}catch(m){f(m)}else d.splice(g,
|
||||
1),g--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var A=new m;return A}]}function Ed(){var b=/^\s*(https?|ftp|mailto|tel|file):/,a=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationWhitelist=function(a){return B(a)?(b=a,this):b};this.imgSrcSanitizationWhitelist=function(b){return B(b)?(a=b,this):a};this.$get=function(){return function(c,d){var e=d?a:b,f;if(!X||8<=X)if(f=
|
||||
Aa(c).href,""!==f&&!f.match(e))return"unsafe:"+f;return c}}}function ef(b){if("self"===b)return b;if(C(b)){if(-1<b.indexOf("***"))throw Ca("iwcard",b);b=b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08").replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+b+"$")}if(cb(b))return new RegExp("^"+b.source+"$");throw Ca("imatcher");}function Sc(b){var a=[];B(b)&&r(b,function(b){a.push(ef(b))});return a}function Fe(){this.SCE_CONTEXTS=ka;var b=["self"],a=[];
|
||||
this.resourceUrlWhitelist=function(a){arguments.length&&(b=Sc(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&(a=Sc(b));return a};this.$get=["$injector",function(c){function d(a,b){return"self"===a?Ic(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}
|
||||
var f=function(a){throw Ca("unsafe");};c.has("$sanitize")&&(f=c.get("$sanitize"));var g=e(),h={};h[ka.HTML]=e(g);h[ka.CSS]=e(g);h[ka.URL]=e(g);h[ka.JS]=e(g);h[ka.RESOURCE_URL]=e(h[ka.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw Ca("icontext",a,b);if(null===b||b===s||""===b)return b;if("string"!==typeof b)throw Ca("itype",a);return new c(b)},getTrusted:function(c,e){if(null===e||e===s||""===e)return e;var g=h.hasOwnProperty(c)?h[c]:null;if(g&&e instanceof g)return e.$$unwrapTrustedValue();
|
||||
if(c===ka.RESOURCE_URL){var g=Aa(e.toString()),p,l,q=!1;p=0;for(l=b.length;p<l;p++)if(d(b[p],g)){q=!0;break}if(q)for(p=0,l=a.length;p<l;p++)if(d(a[p],g)){q=!1;break}if(q)return e;throw Ca("insecurl",e.toString());}if(c===ka.HTML)return f(e);throw Ca("unsafe");},valueOf:function(a){return a instanceof g?a.$$unwrapTrustedValue():a}}}]}function Ee(){var b=!0;this.enabled=function(a){arguments.length&&(b=!!a);return b};this.$get=["$parse","$sniffer","$sceDelegate",function(a,c,d){if(b&&c.msie&&8>c.msieDocumentMode)throw Ca("iequirks");
|
||||
var e=qa(ka);e.isEnabled=function(){return b};e.trustAs=d.trustAs;e.getTrusted=d.getTrusted;e.valueOf=d.valueOf;b||(e.trustAs=e.getTrusted=function(a,b){return b},e.valueOf=Pa);e.parseAs=function(b,c){var d=a(c);return d.literal&&d.constant?d:a(c,function(a){return e.getTrusted(b,a)})};var f=e.parseAs,g=e.getTrusted,h=e.trustAs;r(ka,function(a,b){var c=P(b);e[Wa("parse_as_"+c)]=function(b){return f(a,b)};e[Wa("get_trusted_"+c)]=function(b){return g(a,b)};e[Wa("trust_as_"+c)]=function(b){return h(a,
|
||||
b)}});return e}]}function Ge(){this.$get=["$window","$document",function(b,a){var c={},d=U((/android (\d+)/.exec(P((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},g=f.documentMode,h,m=/^(Moz|webkit|O|ms)(?=[A-Z])/,k=f.body&&f.body.style,n=!1,p=!1;if(k){for(var l in k)if(n=m.exec(l)){h=n[0];h=h.substr(0,1).toUpperCase()+h.substr(1);break}h||(h="WebkitOpacity"in k&&"webkit");n=!!("transition"in k||h+"Transition"in k);p=!!("animation"in k||h+"Animation"in
|
||||
k);!d||n&&p||(n=C(f.body.style.webkitTransition),p=C(f.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hashchange:"onhashchange"in b&&(!g||7<g),hasEvent:function(a){if("input"==a&&9==X)return!1;if(F(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:Ua(),vendorPrefix:h,transitions:n,animations:p,android:d,msie:X,msieDocumentMode:g}}]}function Ie(){this.$get=["$templateCache","$http","$q",function(b,a,c){function d(e,f){function g(){h.totalPendingRequests--;
|
||||
if(!f)throw ja("tpload",e);return c.reject()}var h=d;h.totalPendingRequests++;return a.get(e,{cache:b}).then(function(a){a=a.data;if(!a||0===a.length)return g();h.totalPendingRequests--;b.put(e,a);return a},g)}d.totalPendingRequests=0;return d}]}function Je(){this.$get=["$rootScope","$browser","$location",function(b,a,c){return{findBindings:function(a,b,c){a=a.getElementsByClassName("ng-binding");var g=[];r(a,function(a){var d=Ea.element(a).data("$binding");d&&r(d,function(d){c?(new RegExp("(^|\\s)"+
|
||||
b+"(\\s|\\||$)")).test(d)&&g.push(a):-1!=d.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,c){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var m=a.querySelectorAll("["+g[h]+"model"+(c?"=":"*=")+'"'+b+'"]');if(m.length)return m}},getLocation:function(){return c.url()},setLocation:function(a){a!==c.url()&&(c.url(a),b.$digest())},whenStable:function(b){a.notifyWhenNoOutstandingRequests(b)}}}]}function Ke(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",function(b,
|
||||
a,c,d,e){function f(f,m,k){var n=B(k)&&!k,p=(n?d:c).defer(),l=p.promise;m=a.defer(function(){try{p.resolve(f())}catch(a){p.reject(a),e(a)}finally{delete g[l.$$timeoutId]}n||b.$apply()},m);l.$$timeoutId=m;g[m]=p;return l}var g={};f.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),delete g[b.$$timeoutId],a.defer.cancel(b.$$timeoutId)):!1};return f}]}function Aa(b,a){var c=b;X&&(Z.setAttribute("href",c),c=Z.href);Z.setAttribute("href",c);return{href:Z.href,protocol:Z.protocol?
|
||||
Z.protocol.replace(/:$/,""):"",host:Z.host,search:Z.search?Z.search.replace(/^\?/,""):"",hash:Z.hash?Z.hash.replace(/^#/,""):"",hostname:Z.hostname,port:Z.port,pathname:"/"===Z.pathname.charAt(0)?Z.pathname:"/"+Z.pathname}}function Ic(b){b=C(b)?Aa(b):b;return b.protocol===Tc.protocol&&b.host===Tc.host}function Le(){this.$get=da(t)}function qc(b){function a(c,d){if(S(c)){var e={};r(c,function(b,c){e[c]=a(c,b)});return e}return b.factory(c+"Filter",d)}this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+
|
||||
"Filter")}}];a("currency",Uc);a("date",Vc);a("filter",ff);a("json",gf);a("limitTo",hf);a("lowercase",jf);a("number",Wc);a("orderBy",Xc);a("uppercase",kf)}function ff(){return function(b,a,c){if(!L(b))return b;var d=typeof c,e=[];e.check=function(a,b){for(var c=0;c<e.length;c++)if(!e[c](a,b))return!1;return!0};"function"!==d&&(c="boolean"===d&&c?function(a,b){return Ea.equals(a,b)}:function(a,b){if(a&&b&&"object"===typeof a&&"object"===typeof b){for(var d in a)if("$"!==d.charAt(0)&&Ab.call(a,d)&&c(a[d],
|
||||
b[d]))return!0;return!1}b=(""+b).toLowerCase();return-1<(""+a).toLowerCase().indexOf(b)});var f=function(a,b){if("string"==typeof b&&"!"===b.charAt(0))return!f(a,b.substr(1));switch(typeof a){case "boolean":case "number":case "string":return c(a,b);case "object":switch(typeof b){case "object":return c(a,b);default:for(var d in a)if("$"!==d.charAt(0)&&f(a[d],b))return!0}return!1;case "array":for(d=0;d<a.length;d++)if(f(a[d],b))return!0;return!1;default:return!1}};switch(typeof a){case "boolean":case "number":case "string":a=
|
||||
{$:a};case "object":for(var g in a)(function(b){"undefined"!==typeof a[b]&&e.push(function(c){return f("$"==b?c:c&&c[b],a[b])})})(g);break;case "function":e.push(a);break;default:return b}d=[];for(g=0;g<b.length;g++){var h=b[g];e.check(h,g)&&d.push(h)}return d}}function Uc(b){var a=b.NUMBER_FORMATS;return function(b,d){F(d)&&(d=a.CURRENCY_SYM);return null==b?b:Yc(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,2).replace(/\u00A4/g,d)}}function Wc(b){var a=b.NUMBER_FORMATS;return function(b,d){return null==
|
||||
b?b:Yc(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}}function Yc(b,a,c,d,e){if(!isFinite(b)||S(b))return"";var f=0>b;b=Math.abs(b);var g=b+"",h="",m=[],k=!1;if(-1!==g.indexOf("e")){var n=g.match(/([\d\.]+)e(-?)(\d+)/);n&&"-"==n[2]&&n[3]>e+1?(g="0",b=0):(h=g,k=!0)}if(k)0<e&&-1<b&&1>b&&(h=b.toFixed(e));else{g=(g.split(Zc)[1]||"").length;F(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));b=+(Math.round(+(b.toString()+"e"+e)).toString()+"e"+-e);0===b&&(f=!1);b=(""+b).split(Zc);g=b[0];b=b[1]||"";var n=
|
||||
0,p=a.lgSize,l=a.gSize;if(g.length>=p+l)for(n=g.length-p,k=0;k<n;k++)0===(n-k)%l&&0!==k&&(h+=c),h+=g.charAt(k);for(k=n;k<g.length;k++)0===(g.length-k)%p&&0!==k&&(h+=c),h+=g.charAt(k);for(;b.length<e;)b+="0";e&&"0"!==e&&(h+=d+b.substr(0,e))}m.push(f?a.negPre:a.posPre);m.push(h);m.push(f?a.negSuf:a.posSuf);return m.join("")}function vb(b,a,c){var d="";0>b&&(d="-",b=-b);for(b=""+b;b.length<a;)b="0"+b;c&&(b=b.substr(b.length-a));return d+b}function $(b,a,c,d){c=c||0;return function(e){e=e["get"+b]();
|
||||
if(0<c||e>-c)e+=c;0===e&&-12==c&&(e=12);return vb(e,a,d)}}function wb(b,a){return function(c,d){var e=c["get"+b](),f=ib(a?"SHORT"+b:b);return d[f][e]}}function $c(b){var a=(new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function ad(b){return function(a){var c=$c(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return vb(a,b)}}function Vc(b){function a(a){var b;if(b=a.match(c)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:
|
||||
a.setFullYear,m=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=U(b[9]+b[10]),g=U(b[9]+b[11]));h.call(a,U(b[1]),U(b[2])-1,U(b[3]));f=U(b[4]||0)-f;g=U(b[5]||0)-g;h=U(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));m.call(a,f,g,h,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e,f){var g="",h=[],m,k;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;C(c)&&(c=lf.test(c)?U(c):a(c));ea(c)&&(c=new Date(c));if(!fa(c))return c;
|
||||
for(;e;)(k=mf.exec(e))?(h=db(h,k,1),e=h.pop()):(h.push(e),e=null);f&&"UTC"===f&&(c=new Date(c.getTime()),c.setMinutes(c.getMinutes()+c.getTimezoneOffset()));r(h,function(a){m=nf[a];g+=m?m(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function gf(){return function(b){return sa(b,!0)}}function hf(){return function(b,a){if(!L(b)&&!C(b))return b;a=Infinity===Math.abs(Number(a))?Number(a):U(a);if(C(b))return a?0<=a?b.slice(0,a):b.slice(a,b.length):"";var c=[],d,e;a>b.length?
|
||||
a=b.length:a<-b.length&&(a=-b.length);0<a?(d=0,e=a):(d=b.length+a,e=b.length);for(;d<e;d++)c.push(b[d]);return c}}function Xc(b){return function(a,c,d){function e(a,b){return b?function(b,c){return a(c,b)}:a}function f(a,b){var c=typeof a,d=typeof b;return c==d?(fa(a)&&fa(b)&&(a=a.valueOf(),b=b.valueOf()),"string"==c&&(a=a.toLowerCase(),b=b.toLowerCase()),a===b?0:a<b?-1:1):c<d?-1:1}if(!Na(a)||!c)return a;c=L(c)?c:[c];c=sd(c,function(a){var c=!1,d=a||Pa;if(C(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))c=
|
||||
"-"==a.charAt(0),a=a.substring(1);d=b(a);if(d.constant){var h=d();return e(function(a,b){return f(a[h],b[h])},c)}}return e(function(a,b){return f(d(a),d(b))},c)});for(var g=[],h=0;h<a.length;h++)g.push(a[h]);return g.sort(e(function(a,b){for(var d=0;d<c.length;d++){var e=c[d](a,b);if(0!==e)return e}return 0},d))}}function Fa(b){D(b)&&(b={link:b});b.restrict=b.restrict||"AC";return da(b)}function bd(b,a,c,d){var e=this,f=b.parent().controller("form")||xb,g=[];e.$error={};e.$$success={};e.$pending=
|
||||
s;e.$name=a.name||a.ngForm;e.$dirty=!1;e.$pristine=!0;e.$valid=!0;e.$invalid=!1;e.$submitted=!1;f.$addControl(e);b.addClass(Ma);e.$rollbackViewValue=function(){r(g,function(a){a.$rollbackViewValue()})};e.$commitViewValue=function(){r(g,function(a){a.$commitViewValue()})};e.$addControl=function(a){Ja(a.$name,"input");g.push(a);a.$name&&(e[a.$name]=a)};e.$removeControl=function(a){a.$name&&e[a.$name]===a&&delete e[a.$name];r(e.$pending,function(b,c){e.$setValidity(c,null,a)});r(e.$error,function(b,
|
||||
c){e.$setValidity(c,null,a)});Ra(g,a)};cd({ctrl:this,$element:b,set:function(a,b,c){var d=a[b];d?-1===d.indexOf(c)&&d.push(c):a[b]=[c]},unset:function(a,b,c){var d=a[b];d&&(Ra(d,c),0===d.length&&delete a[b])},parentForm:f,$animate:d});e.$setDirty=function(){d.removeClass(b,Ma);d.addClass(b,yb);e.$dirty=!0;e.$pristine=!1;f.$setDirty()};e.$setPristine=function(){d.setClass(b,Ma,yb+" ng-submitted");e.$dirty=!1;e.$pristine=!0;e.$submitted=!1;r(g,function(a){a.$setPristine()})};e.$setSubmitted=function(){d.addClass(b,
|
||||
"ng-submitted");e.$submitted=!0;f.$setSubmitted()}}function Ub(b){b.$formatters.push(function(a){return b.$isEmpty(a)?a:a.toString()})}function $a(b,a,c,d,e,f){a.prop("validity");var g=a[0].placeholder,h={},m=P(a[0].type);if(!e.android){var k=!1;a.on("compositionstart",function(a){k=!0});a.on("compositionend",function(){k=!1;n()})}var n=function(b){if(!k){var e=a.val(),f=b&&b.type;X&&"input"===(b||h).type&&a[0].placeholder!==g?g=a[0].placeholder:("password"===m||c.ngTrim&&"false"===c.ngTrim||(e=ba(e)),
|
||||
(d.$viewValue!==e||""===e&&d.$$hasNativeValidators)&&d.$setViewValue(e,f))}};if(e.hasEvent("input"))a.on("input",n);else{var p,l=function(a){p||(p=f.defer(function(){n(a);p=null}))};a.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||l(a)});if(e.hasEvent("paste"))a.on("paste cut",l)}a.on("change",n);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)}}function zb(b,a){return function(c){var d;if(fa(c))return c;if(C(c)){'"'==c.charAt(0)&&'"'==c.charAt(c.length-
|
||||
1)&&(c=c.substring(1,c.length-1));if(of.test(c))return new Date(c);b.lastIndex=0;if(c=b.exec(c))return c.shift(),d={yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0},r(c,function(b,c){c<a.length&&(d[a[c]]=+b)}),new Date(d.yyyy,d.MM-1,d.dd,d.HH,d.mm,d.ss||0)}return NaN}}function ab(b,a,c,d){return function(e,f,g,h,m,k,n){function p(a){return B(a)?fa(a)?a:c(a):s}dd(e,f,g,h);$a(e,f,g,h,m,k);var l=h&&h.$options&&h.$options.timezone;h.$$parserName=b;h.$parsers.push(function(b){return h.$isEmpty(b)?null:a.test(b)?(b=
|
||||
c(b),"UTC"===l&&b.setMinutes(b.getMinutes()-b.getTimezoneOffset()),b):s});h.$formatters.push(function(a){return fa(a)?n("date")(a,d,l):""});if(B(g.min)||g.ngMin){var q;h.$validators.min=function(a){return h.$isEmpty(a)||F(q)||c(a)>=q};g.$observe("min",function(a){q=p(a);h.$validate()})}if(B(g.max)||g.ngMax){var r;h.$validators.max=function(a){return h.$isEmpty(a)||F(r)||c(a)<=r};g.$observe("max",function(a){r=p(a);h.$validate()})}}}function dd(b,a,c,d){(d.$$hasNativeValidators=S(a[0].validity))&&
|
||||
d.$parsers.push(function(b){var c=a.prop("validity")||{};return c.badInput&&!c.typeMismatch?s:b})}function ed(b,a,c,d,e){if(B(d)){b=b(d);if(!b.constant)throw K("ngModel")("constexpr",c,d);return b(a)}return e}function cd(b){function a(a,b){b&&!f[a]?(k.addClass(e,a),f[a]=!0):!b&&f[a]&&(k.removeClass(e,a),f[a]=!1)}function c(b,c){b=b?"-"+Db(b,"-"):"";a(pf+b,!0===c);a(qf+b,!1===c)}var d=b.ctrl,e=b.$element,f={},g=b.set,h=b.unset,m=b.parentForm,k=b.$animate;d.$setValidity=function(b,e,f){e===s?(d.$pending||
|
||||
(d.$pending={}),g(d.$pending,b,f)):(d.$pending&&h(d.$pending,b,f),fd(d.$pending)&&(d.$pending=s));"boolean"!==typeof e?(h(d.$error,b,f),h(d.$$success,b,f)):e?(h(d.$error,b,f),g(d.$$success,b,f)):(g(d.$error,b,f),h(d.$$success,b,f));d.$pending?(a(gd,!0),d.$valid=d.$invalid=s,c("",null)):(a(gd,!1),d.$valid=fd(d.$error),d.$invalid=!d.$valid,c("",d.$valid));e=d.$pending&&d.$pending[b]?s:d.$error[b]?!1:d.$$success[b]?!0:null;c(b,e);m.$setValidity(b,e,d)};c("",!0)}function fd(b){if(b)for(var a in b)return!1;
|
||||
return!0}function Vb(b,a){b="ngClass"+b;return["$animate",function(c){function d(a,b){var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],n=0;n<b.length;n++)if(e==b[n])continue a;c.push(e)}return c}function e(a){if(!L(a)){if(C(a))return a.split(" ");if(S(a)){var b=[];r(a,function(a,c){a&&(b=b.concat(c.split(" ")))});return b}}return a}return{restrict:"AC",link:function(f,g,h){function m(a,b){var c=g.data("$classCounts")||{},d=[];r(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)});
|
||||
g.data("$classCounts",c);return d.join(" ")}function k(b){if(!0===a||f.$index%2===a){var k=e(b||[]);if(!n){var q=m(k,1);h.$addClass(q)}else if(!ra(b,n)){var r=e(n),q=d(k,r),k=d(r,k),q=m(q,1),k=m(k,-1);q&&q.length&&c.addClass(g,q);k&&k.length&&c.removeClass(g,k)}}n=qa(b)}var n;f.$watch(h[b],k,!0);h.$observe("class",function(a){k(f.$eval(h[b]))});"ngClass"!==b&&f.$watch("$index",function(c,d){var g=c&1;if(g!==(d&1)){var k=e(f.$eval(h[b]));g===a?(g=m(k,1),h.$addClass(g)):(g=m(k,-1),h.$removeClass(g))}})}}}]}
|
||||
var rf=/^\/(.+)\/([a-z]*)$/,P=function(b){return C(b)?b.toLowerCase():b},Ab=Object.prototype.hasOwnProperty,ib=function(b){return C(b)?b.toUpperCase():b},X,G,la,Ta=[].slice,sf=[].push,Ga=Object.prototype.toString,Sa=K("ng"),Ea=t.angular||(t.angular={}),Va,bb=0;X=U((/msie (\d+)/.exec(P(navigator.userAgent))||[])[1]);isNaN(X)&&(X=U((/trident\/.*; rv:(\d+)/.exec(P(navigator.userAgent))||[])[1]));w.$inject=[];Pa.$inject=[];var L=Array.isArray,ba=function(b){return C(b)?b.trim():b},Ua=function(){if(B(Ua.isActive_))return Ua.isActive_;
|
||||
var b=!(!Y.querySelector("[ng-csp]")&&!Y.querySelector("[data-ng-csp]"));if(!b)try{new Function("")}catch(a){b=!0}return Ua.isActive_=b},fb=["ng-","data-ng-","ng:","x-ng-"],yd=/[A-Z]/g,hc=!1,Eb,Cd={full:"1.3.0-rc.1",major:1,minor:3,dot:0,codeName:"backyard-atomicity"};V.expando="ng339";var ob=V.cache={},Te=1;V._data=function(b){return this.cache[b[this.expando]]||{}};var Oe=/([\:\-\_]+(.))/g,Pe=/^moz([A-Z])/,tf={mouseleave:"mouseout",mouseenter:"mouseover"},Hb=K("jqLite"),Se=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,
|
||||
Gb=/<|&#?\w+;/,Qe=/<([\w:]+)/,Re=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ia={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ia.optgroup=ia.option;ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead;ia.th=ia.td;var Ia=V.prototype={ready:function(b){function a(){c||(c=
|
||||
!0,b())}var c=!1;"complete"===Y.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),V(t).on("load",a),this.on("DOMContentLoaded",a))},toString:function(){var b=[];r(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?G(this[b]):G(this[this.length+b])},length:0,push:sf,sort:[].sort,splice:[].splice},qb={};r("multiple selected checked disabled readOnly required open".split(" "),function(b){qb[P(b)]=b});var zc={};r("input select option textarea button form details".split(" "),
|
||||
function(b){zc[b]=!0});var Ac={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};r({data:Jb,removeData:mb},function(b,a){V[a]=b});r({data:Jb,inheritedData:pb,scope:function(b){return G.data(b,"$scope")||pb(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return G.data(b,"$isolateScope")||G.data(b,"$isolateScopeNoTemplate")},controller:vc,injector:function(b){return pb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:jb,
|
||||
css:function(b,a,c){a=Wa(a);if(B(c))b.style[a]=c;else return b.style[a]},attr:function(b,a,c){var d=P(a);if(qb[d])if(B(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||w).specified?d:s;else if(B(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?s:b},prop:function(b,a,c){if(B(c))b[a]=c;else return b[a]},text:function(){function b(a,b){if(F(b)){var d=a.nodeType;return 1===d||3===d?a.textContent:""}a.textContent=
|
||||
b}b.$dv="";return b}(),val:function(b,a){if(F(a)){if(b.multiple&&"select"===pa(b)){var c=[];r(b.options,function(a){a.selected&&c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(F(a))return b.innerHTML;lb(b,!0);b.innerHTML=a},empty:wc},function(b,a){V.prototype[a]=function(a,d){var e,f,g=this.length;if(b!==wc&&(2==b.length&&b!==jb&&b!==vc?a:d)===s){if(S(a)){for(e=0;e<g;e++)if(b===Jb)b(this[e],a);else for(f in a)b(this[e],f,a[f]);return this}e=b.$dv;
|
||||
g=e===s?Math.min(g,1):g;for(f=0;f<g;f++){var h=b(this[f],a,d);e=e?e+h:h}return e}for(e=0;e<g;e++)b(this[e],a,d);return this}});r({removeData:mb,on:function a(c,d,e,f){if(B(f))throw Hb("onargs");if(rc(c)){var g=nb(c,!0);f=g.events;var h=g.handle;h||(h=g.handle=Ve(c,f));for(var g=0<=d.indexOf(" ")?d.split(" "):[d],m=g.length;m--;){d=g[m];var k=f[d];k||(f[d]=[],"mouseenter"===d||"mouseleave"===d?a(c,tf[d],function(a){var c=a.relatedTarget;c&&(c===this||this.contains(c))||h(a,d)}):"$destroy"!==d&&c.addEventListener(d,
|
||||
h,!1),k=f[d]);k.push(e)}}},off:uc,one:function(a,c,d){a=G(a);a.on(c,function f(){a.off(c,d);a.off(c,f)});a.on(c,d)},replaceWith:function(a,c){var d,e=a.parentNode;lb(a);r(new V(c),function(c){d?e.insertBefore(c,d.nextSibling):e.replaceChild(c,a);d=c})},children:function(a){var c=[];r(a.childNodes,function(a){1===a.nodeType&&c.push(a)});return c},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,c){var d=a.nodeType;if(1===d||11===d){c=new V(c);for(var d=0,e=c.length;d<
|
||||
e;d++)a.appendChild(c[d])}},prepend:function(a,c){if(1===a.nodeType){var d=a.firstChild;r(new V(c),function(c){a.insertBefore(c,d)})}},wrap:function(a,c){c=G(c).eq(0).clone()[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:xc,detach:function(a){xc(a,!0)},after:function(a,c){var d=a,e=a.parentNode;c=new V(c);for(var f=0,g=c.length;f<g;f++){var h=c[f];e.insertBefore(h,d.nextSibling);d=h}},addClass:Lb,removeClass:Kb,toggleClass:function(a,c,d){c&&r(c.split(" "),function(c){var f=
|
||||
d;F(f)&&(f=!jb(a,c));(f?Lb:Kb)(a,c)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,c){return a.getElementsByTagName?a.getElementsByTagName(c):[]},clone:Ib,triggerHandler:function(a,c,d){var e,f;e=c.type||c;var g=nb(a);if(g=(g=g&&g.events)&&g[e])e={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopPropagation:w,type:e,target:a},c.type&&(e=E(e,c)),
|
||||
c=qa(g),f=d?[e].concat(d):[e],r(c,function(c){c.apply(a,f)})}},function(a,c){V.prototype[c]=function(c,e,f){for(var g,h=0,m=this.length;h<m;h++)F(g)?(g=a(this[h],c,e,f),B(g)&&(g=G(g))):tc(g,a(this[h],c,e,f));return B(g)?g:this};V.prototype.bind=V.prototype.on;V.prototype.unbind=V.prototype.off});Xa.prototype={put:function(a,c){this[Ka(a,this.nextUid)]=c},get:function(a){return this[Ka(a,this.nextUid)]},remove:function(a){var c=this[a=Ka(a,this.nextUid)];delete this[a];return c}};var Cc=/^function\s*[^\(]*\(\s*([^\)]*)\)/m,
|
||||
Xe=/,/,Ye=/^\s*(_?)(\S+?)\1\s*$/,Bc=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,La=K("$injector");Cb.$$annotate=Mb;var uf=K("$animate"),oe=["$provide",function(a){this.$$selectors={};this.register=function(c,d){var e=c+"-animation";if(c&&"."!=c.charAt(0))throw uf("notcsel",c);this.$$selectors[c.substr(1)]=e;a.factory(e,d)};this.classNameFilter=function(a){1===arguments.length&&(this.$$classNameFilter=a instanceof RegExp?a:null);return this.$$classNameFilter};this.$get=["$$q","$$asyncCallback",function(a,d){function e(){f||
|
||||
(f=a.defer(),d(function(){f.resolve();f=null}));return f.promise}var f;return{enter:function(a,c,d){d?d.after(a):c.prepend(a);return e()},leave:function(a){a.remove();return e()},move:function(a,c,d){return this.enter(a,c,d)},addClass:function(a,c){c=C(c)?c:L(c)?c.join(" "):"";r(a,function(a){Lb(a,c)});return e()},removeClass:function(a,c){c=C(c)?c:L(c)?c.join(" "):"";r(a,function(a){Kb(a,c)});return e()},setClass:function(a,c,d){this.addClass(a,c);this.removeClass(a,d);return e()},enabled:w,cancel:w}}]}],
|
||||
ja=K("$compile");jc.$inject=["$provide","$$sanitizeUriProvider"];var af=/^(x[\:\-_]|data[\:\-_])/i,Pb=K("$interpolate"),vf=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,df={http:80,https:443,ftp:21},sb=K("$location");Mc.prototype=Sb.prototype=Lc.prototype={$$html5:!1,$$replace:!1,absUrl:tb("$$absUrl"),url:function(a){if(F(a))return this.$$url;a=vf.exec(a);a[1]&&this.path(decodeURIComponent(a[1]));(a[2]||a[1])&&this.search(a[3]||"");this.hash(a[5]||"");return this},protocol:tb("$$protocol"),host:tb("$$host"),
|
||||
port:tb("$$port"),path:Nc("$$path",function(a){a=a?a.toString():"";return"/"==a.charAt(0)?a:"/"+a}),search:function(a,c){switch(arguments.length){case 0:return this.$$search;case 1:if(C(a)||ea(a))a=a.toString(),this.$$search=fc(a);else if(S(a))r(a,function(c,e){null==c&&delete a[e]}),this.$$search=a;else throw sb("isrcharg");break;default:F(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:Nc("$$hash",function(a){return a?a.toString():""}),replace:function(){this.$$replace=
|
||||
!0;return this}};var oa=K("$parse"),wf=Function.prototype.call,xf=Function.prototype.apply,yf=Function.prototype.bind,hd=Object.create(null);r({"null":function(){return null},"true":function(){return!0},"false":function(){return!1},undefined:function(){}},function(a,c){a.constant=a.literal=a.sharedGetter=!0;hd[c]=a});var Wb=E(Object.create(null),{"+":function(a,c,d,e){d=d(a,c);e=e(a,c);return B(d)?B(e)?d+e:d:B(e)?e:s},"-":function(a,c,d,e){d=d(a,c);e=e(a,c);return(B(d)?d:0)-(B(e)?e:0)},"*":function(a,
|
||||
c,d,e){return d(a,c)*e(a,c)},"/":function(a,c,d,e){return d(a,c)/e(a,c)},"%":function(a,c,d,e){return d(a,c)%e(a,c)},"^":function(a,c,d,e){return d(a,c)^e(a,c)},"=":w,"===":function(a,c,d,e){return d(a,c)===e(a,c)},"!==":function(a,c,d,e){return d(a,c)!==e(a,c)},"==":function(a,c,d,e){return d(a,c)==e(a,c)},"!=":function(a,c,d,e){return d(a,c)!=e(a,c)},"<":function(a,c,d,e){return d(a,c)<e(a,c)},">":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,
|
||||
c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}}),zf={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Tb=function(a){this.options=a};Tb.prototype={constructor:Tb,lex:function(a){this.text=a;this.index=0;this.ch=s;for(this.tokens=[];this.index<this.text.length;)if(this.ch=this.text.charAt(this.index),
|
||||
this.is("\"'"))this.readString(this.ch);else if(this.isNumber(this.ch)||this.is(".")&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(this.ch))this.readIdent();else if(this.is("(){}[].,;:?"))this.tokens.push({index:this.index,text:this.ch}),this.index++;else if(this.isWhitespace(this.ch))this.index++;else{a=this.ch+this.peek();var c=a+this.peek(2),d=Wb[this.ch],e=Wb[a],f=Wb[c];f?(this.tokens.push({index:this.index,text:c,fn:f}),this.index+=3):e?(this.tokens.push({index:this.index,
|
||||
text:a,fn:e}),this.index+=2):d?(this.tokens.push({index:this.index,text:this.ch,fn:d}),this.index+=1):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a){return-1!==a.indexOf(this.ch)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=
|
||||
a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d=d||this.index;c=B(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw oa("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index<this.text.length;){var d=P(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var e=this.peek();if("e"==d&&this.isExpOperator(e))a+=d;else if(this.isExpOperator(d)&&
|
||||
e&&this.isNumber(e)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||e&&this.isNumber(e)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}a*=1;this.tokens.push({index:c,text:a,constant:!0,fn:function(){return a}})},readIdent:function(){for(var a=this.text,c="",d=this.index,e,f,g,h;this.index<this.text.length;){h=this.text.charAt(this.index);if("."===h||this.isIdent(h)||this.isNumber(h))"."===h&&(e=this.index),c+=h;else break;this.index++}e&&"."===
|
||||
c[c.length-1]&&(this.index--,c=c.slice(0,-1),e=c.lastIndexOf("."),-1===e&&(e=s));if(e)for(f=this.index;f<this.text.length;){h=this.text.charAt(f);if("("===h){g=c.substr(e-d+1);c=c.substr(0,e-d);this.index=f;break}if(this.isWhitespace(h))f++;else break}this.tokens.push({index:d,text:c,fn:hd[c]||Pc(c,this.options,a)});g&&(this.tokens.push({index:e,text:"."}),this.tokens.push({index:e+1,text:g}))},readString:function(a){var c=this.index;this.index++;for(var d="",e=a,f=!1;this.index<this.text.length;){var g=
|
||||
this.text.charAt(this.index),e=e+g;if(f)"u"===g?(f=this.text.substring(this.index+1,this.index+5),f.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+f+"]"),this.index+=4,d+=String.fromCharCode(parseInt(f,16))):d+=zf[g]||g,f=!1;else if("\\"===g)f=!0;else{if(g===a){this.index++;this.tokens.push({index:c,text:e,string:d,constant:!0,fn:function(){return d}});return}d+=g}this.index++}this.throwError("Unterminated quote",c)}};var Za=function(a,c,d){this.lexer=a;this.$filter=c;this.options=
|
||||
d};Za.ZERO=E(function(){return 0},{sharedGetter:!0,constant:!0});Za.prototype={constructor:Za,parse:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.statements();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);a.literal=!!a.literal;a.constant=!!a.constant;return a},primary:function(){var a;if(this.expect("("))a=this.filterChain(),this.consume(")");else if(this.expect("["))a=this.arrayDeclaration();else if(this.expect("{"))a=this.object();else{var c=this.expect();
|
||||
(a=c.fn)||this.throwError("not a primary expression",c);c.constant&&(a.constant=!0,a.literal=!0)}for(var d;c=this.expect("(","[",".");)"("===c.text?(a=this.functionCall(a,d),d=null):"["===c.text?(d=a,a=this.objectIndex(a)):"."===c.text?(d=a,a=this.fieldAccess(a)):this.throwError("IMPOSSIBLE");return a},throwError:function(a,c){throw oa("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},peekToken:function(){if(0===this.tokens.length)throw oa("ueoe",this.text);return this.tokens[0]},
|
||||
peek:function(a,c,d,e){if(0<this.tokens.length){var f=this.tokens[0],g=f.text;if(g===a||g===c||g===d||g===e||!(a||c||d||e))return f}return!1},expect:function(a,c,d,e){return(a=this.peek(a,c,d,e))?(this.tokens.shift(),a):!1},consume:function(a){this.expect(a)||this.throwError("is unexpected, expecting ["+a+"]",this.peek())},unaryFn:function(a,c){return E(function(d,e){return a(d,e,c)},{constant:c.constant})},ternaryFn:function(a,c,d){return E(function(e,f){return a(e,f)?c(e,f):d(e,f)},{constant:a.constant&&
|
||||
c.constant&&d.constant})},binaryFn:function(a,c,d){return E(function(e,f){return c(e,f,a,d)},{constant:a.constant&&d.constant})},statements:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.filterChain()),!this.expect(";"))return 1===a.length?a[0]:function(c,d){for(var e,f=0,g=a.length;f<g;f++)e=a[f](c,d);return e}},filterChain:function(){for(var a=this.expression(),c;c=this.expect("|");)a=this.binaryFn(a,c.fn,this.filter());return a},filter:function(){var a=
|
||||
this.expect(),c=this.$filter(a.text),d,e;if(this.peek(":"))for(d=[],e=[];this.expect(":");)d.push(this.expression());return da(function(a,g,h){if(e){e[0]=h;for(h=d.length;h--;)e[h+1]=d[h](a,g);return c.apply(s,e)}return c(h)})},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary(),c,d;return(d=this.expect("="))?(a.assign||this.throwError("implies assignment but ["+this.text.substring(0,d.index)+"] can not be assigned to",d),c=this.ternary(),function(d,f){return a.assign(d,
|
||||
c(d,f),f)}):a},ternary:function(){var a=this.logicalOR(),c,d;if(this.expect("?")){c=this.assignment();if(d=this.expect(":"))return this.ternaryFn(a,c,this.assignment());this.throwError("expected :",d)}else return a},logicalOR:function(){for(var a=this.logicalAND(),c;c=this.expect("||");)a=this.binaryFn(a,c.fn,this.logicalAND());return a},logicalAND:function(){var a=this.equality(),c;if(c=this.expect("&&"))a=this.binaryFn(a,c.fn,this.logicalAND());return a},equality:function(){var a=this.relational(),
|
||||
c;if(c=this.expect("==","!=","===","!=="))a=this.binaryFn(a,c.fn,this.equality());return a},relational:function(){var a=this.additive(),c;if(c=this.expect("<",">","<=",">="))a=this.binaryFn(a,c.fn,this.relational());return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.fn,this.multiplicative());return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.fn,this.unary());return a},unary:function(){var a;
|
||||
return this.expect("+")?this.primary():(a=this.expect("-"))?this.binaryFn(Za.ZERO,a.fn,this.unary()):(a=this.expect("!"))?this.unaryFn(a.fn,this.unary()):this.primary()},fieldAccess:function(a){var c=this.text,d=this.expect().text,e=Pc(d,this.options,c);return E(function(c,d,h){return e(h||a(c,d))},{assign:function(e,g,h){(h=a(e,h))||a.assign(e,h={});return ub(h,d,g,c)}})},objectIndex:function(a){var c=this.text,d=this.expression();this.consume("]");return E(function(e,f){var g=a(e,f),h=d(e,f);na(h,
|
||||
c);return g?Ba(g[h],c):s},{assign:function(e,f,g){var h=na(d(e,g),c);(g=Ba(a(e,g),c))||a.assign(e,g={});return g[h]=f}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression());while(this.expect(","))}this.consume(")");var e=this.text,f=d.length?[]:null;return function(g,h){var m=c?c(g,h):g,k=a(g,h,m)||w;if(f)for(var n=d.length;n--;)f[n]=Ba(d[n](g,h),e);Ba(m,e);if(k){if(k.constructor===k)throw oa("isecfn",e);if(k===wf||k===xf||k===yf)throw oa("isecff",e);
|
||||
}m=k.apply?k.apply(m,f):k(f[0],f[1],f[2],f[3],f[4]);return Ba(m,e)}},arrayDeclaration:function(){var a=[],c=!0;if("]"!==this.peekToken().text){do{if(this.peek("]"))break;var d=this.expression();a.push(d);d.constant||(c=!1)}while(this.expect(","))}this.consume("]");return E(function(c,d){for(var g=[],h=0,m=a.length;h<m;h++)g.push(a[h](c,d));return g},{literal:!0,constant:c})},object:function(){var a=[],c=!0;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;var d=this.expect(),d=d.string||
|
||||
d.text;this.consume(":");var e=this.expression();a.push({key:d,value:e});e.constant||(c=!1)}while(this.expect(","))}this.consume("}");return E(function(c,d){for(var e={},m=0,k=a.length;m<k;m++){var n=a[m];e[n.key]=n.value(c,d)}return e},{literal:!0,constant:c})}};var Qc=Object.create(null),Ca=K("$sce"),ka={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},ja=K("$compile"),Z=Y.createElement("a"),Tc=Aa(t.location.href,!0);qc.$inject=["$provide"];Uc.$inject=["$locale"];Wc.$inject=["$locale"];
|
||||
var Zc=".",nf={yyyy:$("FullYear",4),yy:$("FullYear",2,0,!0),y:$("FullYear",1),MMMM:wb("Month"),MMM:wb("Month",!0),MM:$("Month",2,1),M:$("Month",1,1),dd:$("Date",2),d:$("Date",1),HH:$("Hours",2),H:$("Hours",1),hh:$("Hours",2,-12),h:$("Hours",1,-12),mm:$("Minutes",2),m:$("Minutes",1),ss:$("Seconds",2),s:$("Seconds",1),sss:$("Milliseconds",3),EEEE:wb("Day"),EEE:wb("Day",!0),a:function(a,c){return 12>a.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(vb(Math[0<
|
||||
a?"floor":"ceil"](a/60),2)+vb(Math.abs(a%60),2))},ww:ad(2),w:ad(1)},mf=/((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/,lf=/^\-?\d+$/;Vc.$inject=["$locale"];var jf=da(P),kf=da(ib);Xc.$inject=["$parse"];var Fd=da({restrict:"E",compile:function(a,c){8>=X&&(c.href||c.name||c.$set("href",""),a.append(Y.createComment("IE fix")));if(!c.href&&!c.xlinkHref&&!c.name)return function(a,c){var f="[object SVGAnimatedString]"===Ga.call(c.prop("href"))?"xlink:href":"href";c.on("click",
|
||||
function(a){c.attr(f)||a.preventDefault()})}}}),kb={};r(qb,function(a,c){if("multiple"!=a){var d=va("ng-"+c);kb[d]=function(){return{restrict:"A",priority:100,link:function(a,f,g){a.$watch(g[d],function(a){g.$set(c,!!a)})}}}}});r(Ac,function(a,c){kb[c]=function(){return{priority:100,link:function(a,e,f){if("ngPattern"===c&&"/"==f.ngPattern.charAt(0)&&(e=f.ngPattern.match(rf))){f.$set("ngPattern",new RegExp(e[1],e[2]));return}a.$watch(f[c],function(a){f.$set(c,a)})}}}});r(["src","srcset","href"],function(a){var c=
|
||||
va("ng-"+a);kb[c]=function(){return{priority:99,link:function(d,e,f){var g=a,h=a;"href"===a&&"[object SVGAnimatedString]"===Ga.call(e.prop("href"))&&(h="xlinkHref",f.$attr[h]="xlink:href",g=null);f.$observe(c,function(c){c?(f.$set(h,c),X&&g&&e.prop(g,f[h])):"href"===a&&f.$set(h,null)})}}}});var xb={$addControl:w,$removeControl:w,$setValidity:w,$$setPending:w,$setDirty:w,$setPristine:w,$setSubmitted:w,$$clearControlValidity:w};bd.$inject=["$element","$attrs","$scope","$animate"];var id=function(a){return["$timeout",
|
||||
function(c){return{name:"form",restrict:a?"EAC":"E",controller:bd,compile:function(){return{pre:function(a,e,f,g){if(!f.action){var h=function(c){a.$apply(function(){g.$commitViewValue();g.$setSubmitted()});c.preventDefault?c.preventDefault():c.returnValue=!1};e[0].addEventListener("submit",h,!1);e.on("$destroy",function(){c(function(){e[0].removeEventListener("submit",h,!1)},0,!1)})}var m=e.parent().controller("form"),k=f.name||f.ngForm;k&&ub(a,k,g,k);if(m)e.on("$destroy",function(){m.$removeControl(g);
|
||||
k&&ub(a,k,s,k);E(g,xb)})}}}}}]},Gd=id(),Td=id(!0),of=/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/,Af=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,Bf=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,Cf=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,jd=/^(\d{4})-(\d{2})-(\d{2})$/,kd=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d))?$/,Xb=/^(\d{4})-W(\d\d)$/,ld=/^(\d{4})-(\d\d)$/,md=/^(\d\d):(\d\d)(?::(\d\d))?$/,
|
||||
Df=/(\s+|^)default(\s+|$)/,Yb=new K("ngModel"),nd={text:function(a,c,d,e,f,g){$a(a,c,d,e,f,g);Ub(e)},date:ab("date",jd,zb(jd,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ab("datetimelocal",kd,zb(kd,"yyyy MM dd HH mm ss".split(" ")),"yyyy-MM-ddTHH:mm:ss"),time:ab("time",md,zb(md,["HH","mm","ss"]),"HH:mm:ss"),week:ab("week",Xb,function(a){if(fa(a))return a;if(C(a)){Xb.lastIndex=0;var c=Xb.exec(a);if(c){a=+c[1];var d=+c[2],c=$c(a),d=7*(d-1);return new Date(a,0,c.getDate()+d)}}return NaN},"yyyy-Www"),
|
||||
month:ab("month",ld,zb(ld,["yyyy","MM"]),"yyyy-MM"),number:function(a,c,d,e,f,g){dd(a,c,d,e);$a(a,c,d,e,f,g);e.$$parserName="number";e.$parsers.push(function(a){return e.$isEmpty(a)?null:Cf.test(a)?parseFloat(a):s});e.$formatters.push(function(a){if(!e.$isEmpty(a)){if(!ea(a))throw Yb("numfmt",a);a=a.toString()}return a});if(d.min||d.ngMin){var h;e.$validators.min=function(a){return e.$isEmpty(a)||F(h)||a>=h};d.$observe("min",function(a){B(a)&&!ea(a)&&(a=parseFloat(a,10));h=ea(a)&&!isNaN(a)?a:s;e.$validate()})}if(d.max||
|
||||
d.ngMax){var m;e.$validators.max=function(a){return e.$isEmpty(a)||F(m)||a<=m};d.$observe("max",function(a){B(a)&&!ea(a)&&(a=parseFloat(a,10));m=ea(a)&&!isNaN(a)?a:s;e.$validate()})}},url:function(a,c,d,e,f,g){$a(a,c,d,e,f,g);Ub(e);e.$$parserName="url";e.$validators.url=function(a,c){var d=a||c;return e.$isEmpty(d)||Af.test(d)}},email:function(a,c,d,e,f,g){$a(a,c,d,e,f,g);Ub(e);e.$$parserName="email";e.$validators.email=function(a,c){var d=a||c;return e.$isEmpty(d)||Bf.test(d)}},radio:function(a,
|
||||
c,d,e){F(d.name)&&c.attr("name",++bb);c.on("click",function(a){c[0].checked&&e.$setViewValue(d.value,a&&a.type)});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e,f,g,h,m){var k=ed(m,a,"ngTrueValue",d.ngTrueValue,!0),n=ed(m,a,"ngFalseValue",d.ngFalseValue,!1);c.on("click",function(a){e.$setViewValue(c[0].checked,a&&a.type)});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return a!==k};e.$formatters.push(function(a){return ra(a,
|
||||
k)});e.$parsers.push(function(a){return a?k:n})},hidden:w,button:w,submit:w,reset:w,file:w},kc=["$browser","$sniffer","$filter","$parse",function(a,c,d,e){return{restrict:"E",require:["?ngModel"],link:function(f,g,h,m){m[0]&&(nd[P(h.type)]||nd.text)(f,g,h,m[0],c,a,d,e)}}}],pf="ng-valid",qf="ng-invalid",Ma="ng-pristine",yb="ng-dirty",gd="ng-pending",Ef=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q",function(a,c,d,e,f,g,h,m,k){this.$modelValue=this.$viewValue=
|
||||
Number.NaN;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=s;this.$name=d.name;var n=f(d.ngModel),p=null,l=this,q=function(){var c=n(a);l.$options&&l.$options.getterSetter&&D(c)&&(c=c());return c},A=function(c){var d;l.$options&&l.$options.getterSetter&&D(d=n(a))?d(l.$modelValue):n.assign(a,
|
||||
l.$modelValue)};this.$$setOptions=function(a){l.$options=a;if(!(n.assign||a&&a.getterSetter))throw Yb("nonassign",d.ngModel,ta(e));};this.$render=w;this.$isEmpty=function(a){return F(a)||""===a||null===a||a!==a};var u=e.inheritedData("$formController")||xb,x=0;e.addClass(Ma).addClass("ng-untouched");cd({ctrl:this,$element:e,set:function(a,c){a[c]=!0},unset:function(a,c){delete a[c]},parentForm:u,$animate:g});this.$setPristine=function(){l.$dirty=!1;l.$pristine=!0;g.removeClass(e,yb);g.addClass(e,
|
||||
Ma)};this.$setUntouched=function(){l.$touched=!1;l.$untouched=!0;g.setClass(e,"ng-untouched","ng-touched")};this.$setTouched=function(){l.$touched=!0;l.$untouched=!1;g.setClass(e,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){h.cancel(p);l.$viewValue=l.$$lastCommittedViewValue;l.$render()};this.$validate=function(){ea(l.$modelValue)&&isNaN(l.$modelValue)||this.$$parseAndValidate()};this.$$runValidators=function(a,c,d,e){function f(){var a=!0;r(l.$validators,function(e,f){var h=e(c,
|
||||
d);a=a&&h;g(f,h)});return a?!0:(r(l.$asyncValidators,function(a,c){g(c,null)}),m(),!1)}function h(){var a=[];r(l.$asyncValidators,function(e,f){var h=e(c,d);if(!h||!D(h.then))throw Yb("$asyncValidators",h);g(f,s);a.push(h.then(function(){g(f,!0)},function(a){g(f,!1)}))});a.length?k.all(a).then(m):m()}function g(a,c){n===x&&l.$setValidity(a,c)}function m(){n===x&&e()}x++;var n=x;(function(a){var c=l.$$parserName||"parse";if(a===s)g(c,null);else if(g(c,a),!a)return r(l.$validators,function(a,c){g(c,
|
||||
null)}),r(l.$asyncValidators,function(a,c){g(c,null)}),m(),!1;return!0})(a)&&f()&&h()};this.$commitViewValue=function(){var a=l.$viewValue;h.cancel(p);if(l.$$lastCommittedViewValue!==a||""===a&&l.$$hasNativeValidators)l.$$lastCommittedViewValue=a,l.$pristine&&(l.$dirty=!0,l.$pristine=!1,g.removeClass(e,Ma),g.addClass(e,yb),u.$setDirty()),this.$$parseAndValidate()};this.$$parseAndValidate=function(){for(var a=!0,c=l.$$lastCommittedViewValue,d=c,e=0;e<l.$parsers.length;e++)if(d=l.$parsers[e](d),F(d)){a=
|
||||
!1;break}ea(l.$modelValue)&&isNaN(l.$modelValue)&&(l.$modelValue=q());var f=l.$modelValue,h=l.$options&&l.$options.allowInvalid;h&&(l.$modelValue=d,l.$modelValue!==f&&l.$$writeModelToScope());l.$$runValidators(a,d,c,function(){h||(l.$modelValue=l.$valid?d:s,l.$modelValue!==f&&l.$$writeModelToScope())})};this.$$writeModelToScope=function(){A(l.$modelValue);r(l.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};this.$setViewValue=function(a,c){l.$viewValue=a;l.$options&&!l.$options.updateOnDefault||
|
||||
l.$$debounceViewValueCommit(c)};this.$$debounceViewValueCommit=function(c){var d=0,e=l.$options;e&&B(e.debounce)&&(e=e.debounce,ea(e)?d=e:ea(e[c])?d=e[c]:ea(e["default"])&&(d=e["default"]));h.cancel(p);d?p=h(function(){l.$commitViewValue()},d):m.$$phase?l.$commitViewValue():a.$apply(function(){l.$commitViewValue()})};a.$watch(function(){var a=q();if(a!==l.$modelValue){l.$modelValue=a;for(var c=l.$formatters,d=c.length,e=a;d--;)e=c[d](e);l.$viewValue!==e&&(l.$viewValue=l.$$lastCommittedViewValue=e,
|
||||
l.$render(),l.$$runValidators(s,a,e,w))}return a})}],he=function(){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:Ef,link:{pre:function(a,c,d,e){var f=e[0],g=e[1]||xb;f.$$setOptions(e[2]&&e[2].$options);g.$addControl(f);a.$on("$destroy",function(){g.$removeControl(f)})},post:function(a,c,d,e){var f=e[0];if(f.$options&&f.$options.updateOn)c.on(f.$options.updateOn,function(a){f.$$debounceViewValueCommit(a&&a.type)});c.on("blur",function(c){f.$touched||a.$apply(function(){f.$setTouched()})})}}}},
|
||||
je=da({restrict:"A",require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),mc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){e&&(d.required=!0,e.$validators.required=function(a,c){return!d.required||!e.$isEmpty(c)},d.$observe("required",function(){e.$validate()}))}}},lc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f,g=d.ngPattern||d.pattern;d.$observe("pattern",function(a){C(a)&&0<a.length&&
|
||||
(a=new RegExp(a));if(a&&!a.test)throw K("ngPattern")("noregexp",g,a,ta(c));f=a||s;e.$validate()});e.$validators.pattern=function(a){return e.$isEmpty(a)||F(f)||f.test(a)}}}}},oc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("maxlength",function(a){f=U(a)||0;e.$validate()});e.$validators.maxlength=function(a,c){return e.$isEmpty(c)||c.length<=f}}}}},nc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("minlength",
|
||||
function(a){f=U(a)||0;e.$validate()});e.$validators.minlength=function(a,c){return e.$isEmpty(c)||c.length>=f}}}}},ie=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,c,d,e){var f=c.attr(d.$attr.ngList)||", ",g="false"!==d.ngTrim,h=g?ba(f):f;e.$parsers.push(function(a){if(!F(a)){var c=[];a&&r(a.split(h),function(a){a&&c.push(g?ba(a):a)});return c}});e.$formatters.push(function(a){return L(a)?a.join(f):s});e.$isEmpty=function(a){return!a||!a.length}}}},Ff=/^(true|false|\d+)$/,
|
||||
ke=function(){return{restrict:"A",priority:100,compile:function(a,c){return Ff.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue,function(a){f.$set("value",a)})}}}},le=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,c){var d=this;this.$options=a.$eval(c.ngModelOptions);this.$options.updateOn!==s?(this.$options.updateOnDefault=!1,this.$options.updateOn=ba(this.$options.updateOn.replace(Df,function(){d.$options.updateOnDefault=
|
||||
!0;return" "}))):this.$options.updateOnDefault=!0}]}},Ld=["$compile",function(a){return{restrict:"AC",compile:function(c){a.$$addBindingClass(c);return function(c,e,f){a.$$addBindingInfo(e,f.ngBind);c.$watch(f.ngBind,function(a){e.text(a==s?"":a)})}}}}],Nd=["$interpolate","$compile",function(a,c){return{compile:function(d){c.$$addBindingClass(d);return function(d,f,g){d=a(f.attr(g.$attr.ngBindTemplate));c.$$addBindingInfo(f,d.expressions);g.$observe("ngBindTemplate",function(a){f.text(a)})}}}}],Md=
|
||||
["$sce","$parse","$compile",function(a,c,d){return{restrict:"A",compile:function(e,f){var g=c(f.ngBindHtml),h=c(f.ngBindHtml,function(a){return(a||"").toString()});d.$$addBindingClass(e);return function(c,e,f){d.$$addBindingInfo(e,f.ngBindHtml);c.$watch(h,function(){e.html(a.getTrustedHtml(g(c))||"")})}}}}],Od=Vb("",!0),Qd=Vb("Odd",0),Pd=Vb("Even",1),Rd=Fa({compile:function(a,c){c.$set("ngCloak",s);a.removeClass("ng-cloak")}}),Sd=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],
|
||||
pc={},Gf={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=va("ng-"+a);pc[c]=["$parse","$rootScope",function(d,e){return{restrict:"A",compile:function(f,g){var h=d(g[c]);return function(c,d){var f=P(a);d.on(f,function(a){var d=function(){h(c,{$event:a})};Gf[f]&&e.$$phase?c.$evalAsync(d):c.$apply(d)})}}}}]});var Vd=["$animate",function(a){return{multiElement:!0,
|
||||
transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,f,g){var h,m,k;c.$watch(e.ngIf,function(c){c?m||g(function(c,f){m=f;c[c.length++]=Y.createComment(" end ngIf: "+e.ngIf+" ");h={clone:c};a.enter(c,d.parent(),d)}):(k&&(k.remove(),k=null),m&&(m.$destroy(),m=null),h&&(k=hb(h.clone),a.leave(k).then(function(){k=null}),h=null))})}}}],Wd=["$templateRequest","$anchorScroll","$animate","$sce",function(a,c,d,e){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",
|
||||
controller:Ea.noop,compile:function(f,g){var h=g.ngInclude||g.src,m=g.onload||"",k=g.autoscroll;return function(f,g,l,q,r){var u=0,s,z,t,v=function(){z&&(z.remove(),z=null);s&&(s.$destroy(),s=null);t&&(d.leave(t).then(function(){z=null}),z=t,t=null)};f.$watch(e.parseAsResourceUrl(h),function(e){var h=function(){!B(k)||k&&!f.$eval(k)||c()},l=++u;e?(a(e,!0).then(function(a){if(l===u){var c=f.$new();q.template=a;a=r(c,function(a){v();d.enter(a,null,g).then(h)});s=c;t=a;s.$emit("$includeContentLoaded");
|
||||
f.$eval(m)}},function(){l===u&&(v(),f.$emit("$includeContentError"))}),f.$emit("$includeContentRequested")):(v(),q.template=null)})}}}}],me=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,f){/SVG/.test(d[0].toString())?(d.empty(),a(sc(f.template,Y).childNodes)(c,function(a){d.append(a)},s,s,d)):(d.html(f.template),a(d.contents())(c))}}}],Xd=Fa({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Yd=Fa({terminal:!0,priority:1E3}),
|
||||
Zd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,f,g){var h=g.count,m=g.$attr.when&&f.attr(g.$attr.when),k=g.offset||0,n=e.$eval(m)||{},p={},l=c.startSymbol(),q=c.endSymbol(),s=/^when(Minus)?(.+)$/;r(g,function(a,c){s.test(c)&&(n[P(c.replace("when","").replace("Minus","-"))]=f.attr(g.$attr[c]))});r(n,function(a,e){p[e]=c(a.replace(d,l+h+"-"+k+q))});e.$watch(function(){var c=parseFloat(e.$eval(h));if(isNaN(c))return"";c in n||(c=a.pluralCat(c-k));return p[c](e)},
|
||||
function(a){f.text(a)})}}}],$d=["$parse","$animate",function(a,c){var d=K("ngRepeat"),e=function(a,c,d,e,k,n,p){a[d]=e;k&&(a[k]=n);a.$index=c;a.$first=0===c;a.$last=c===p-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(c&1))};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,g){var h=g.ngRepeat,m=Y.createComment(" end ngRepeat: "+h+" "),k=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
|
||||
if(!k)throw d("iexp",h);var n=k[1],p=k[2],l=k[3],q=k[4],k=n.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!k)throw d("iidexp",n);var A=k[3]||k[1],u=k[2];if(l&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(l)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent)$/.test(l)))throw d("badident",l);var t,z,B,v,w={$id:Ka};q?t=a(q):(B=function(a,c){return Ka(c)},v=function(a){return a});return function(a,f,g,k,n){t&&(z=function(c,d,e){u&&(w[u]=c);w[A]=d;w.$index=e;return t(a,w)});
|
||||
var q=Object.create(null);a.$watchCollection(p,function(g){var k,p,t=f[0],J,x=Object.create(null),w,N,E,F,y,C,ga;l&&(a[l]=g);if(Na(g))y=g,p=z||B;else{p=z||v;y=[];for(ga in g)g.hasOwnProperty(ga)&&"$"!=ga.charAt(0)&&y.push(ga);y.sort()}w=y.length;ga=Array(w);for(k=0;k<w;k++)if(N=g===y?k:y[k],E=g[N],F=p(N,E,k),q[F])C=q[F],delete q[F],x[F]=C,ga[k]=C;else{if(x[F])throw r(ga,function(a){a&&a.scope&&(q[a.id]=a)}),d("dupes",h,F,sa(E));ga[k]={id:F,scope:s,clone:s};x[F]=!0}for(J in q){C=q[J];F=hb(C.clone);
|
||||
c.leave(F);if(F[0].parentNode)for(k=0,p=F.length;k<p;k++)F[k].$$NG_REMOVED=!0;C.scope.$destroy()}for(k=0;k<w;k++)if(N=g===y?k:y[k],E=g[N],C=ga[k],C.scope){J=t;do J=J.nextSibling;while(J&&J.$$NG_REMOVED);C.clone[0]!=J&&c.move(hb(C.clone),null,G(t));t=C.clone[C.clone.length-1];e(C.scope,k,A,E,u,N,w)}else n(function(a,d){C.scope=d;var f=m.cloneNode(!1);a[a.length++]=f;c.enter(a,null,G(t));t=f;C.clone=a;x[C.id]=C;e(C.scope,k,A,E,u,N,w)});q=x})}}}}],ae=["$animate",function(a){return{restrict:"A",multiElement:!0,
|
||||
link:function(c,d,e){c.$watch(e.ngShow,function(c){a[c?"removeClass":"addClass"](d,"ng-hide")})}}}],Ud=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(c,d,e){c.$watch(e.ngHide,function(c){a[c?"addClass":"removeClass"](d,"ng-hide")})}}}],be=Fa(function(a,c,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&r(d,function(a,d){c.css(d,"")});a&&c.css(a)},!0)}),ce=["$animate",function(a){return{restrict:"EA",require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c,
|
||||
d,e,f){var g=[],h=[],m=[],k=[],n=function(a,c){return function(){a.splice(c,1)}};c.$watch(e.ngSwitch||e.on,function(c){var d,e;d=0;for(e=m.length;d<e;++d)a.cancel(m[d]);d=m.length=0;for(e=k.length;d<e;++d){var s=hb(h[d].clone);k[d].$destroy();(m[d]=a.leave(s)).then(n(m,d))}h.length=0;k.length=0;(g=f.cases["!"+c]||f.cases["?"])&&r(g,function(c){c.transclude(function(d,e){k.push(e);var f=c.element;d[d.length++]=Y.createComment(" end ngSwitchWhen: ");h.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],
|
||||
de=Fa({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,f){e.cases["!"+d.ngSwitchWhen]=e.cases["!"+d.ngSwitchWhen]||[];e.cases["!"+d.ngSwitchWhen].push({transclude:f,element:c})}}),ee=Fa({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,f){e.cases["?"]=e.cases["?"]||[];e.cases["?"].push({transclude:f,element:c})}}),ge=Fa({restrict:"EAC",link:function(a,c,d,e,f){if(!f)throw K("ngTransclude")("orphan",ta(c));f(function(a){c.empty();
|
||||
c.append(a)})}}),Hd=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],Hf=K("ngOptions"),fe=da({restrict:"A",terminal:!0}),Id=["$compile","$parse",function(a,c){var d=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,e={$setViewValue:w};return{restrict:"E",require:["select",
|
||||
"?ngModel"],controller:["$element","$scope","$attrs",function(a,c,d){var m=this,k={},n=e,p;m.databound=d.ngModel;m.init=function(a,c,d){n=a;p=d};m.addOption=function(c,d){Ja(c,'"option value"');k[c]=!0;n.$viewValue==c&&(a.val(c),p.parent()&&p.remove());d[0].hasAttribute("selected")&&(d[0].selected=!0)};m.removeOption=function(a){this.hasOption(a)&&(delete k[a],n.$viewValue==a&&this.renderUnknownOption(a))};m.renderUnknownOption=function(c){c="? "+Ka(c)+" ?";p.val(c);a.prepend(p);a.val(c);p.prop("selected",
|
||||
!0)};m.hasOption=function(a){return k.hasOwnProperty(a)};c.$on("$destroy",function(){m.renderUnknownOption=w})}],link:function(e,g,h,m){function k(a,c,d,e){d.$render=function(){var a=d.$viewValue;e.hasOption(a)?(y.parent()&&y.remove(),c.val(a),""===a&&w.prop("selected",!0)):F(a)&&w?c.val(""):e.renderUnknownOption(a)};c.on("change",function(){a.$apply(function(){y.parent()&&y.remove();d.$setViewValue(c.val())})})}function n(a,c,d){var e;d.$render=function(){var a=new Xa(d.$viewValue);r(c.find("option"),
|
||||
function(c){c.selected=B(a.get(c.value))})};a.$watch(function(){ra(e,d.$viewValue)||(e=qa(d.$viewValue),d.$render())});c.on("change",function(){a.$apply(function(){var a=[];r(c.find("option"),function(c){c.selected&&a.push(c.value)});d.$setViewValue(a)})})}function p(e,f,h){function g(){z||(e.$$postDigest(k),z=!0)}function k(){z=!1;var a={"":[]},c=[""],d,g,l,s,t;l=h.$modelValue;s=F(e)||[];var A=p?Zb(s):s,G,y,D;y={};D=!1;if(q)if(g=h.$modelValue,x&&L(g))for(D=new Xa([]),d={},t=0;t<g.length;t++)d[n]=
|
||||
g[t],D.put(x(e,d),g[t]);else D=new Xa(g);t=D;var H,K;for(D=0;G=A.length,D<G;D++){g=D;if(p){g=A[D];if("$"===g.charAt(0))continue;y[p]=g}y[n]=s[g];d=r(e,y)||"";(g=a[d])||(g=a[d]=[],c.push(d));q?d=B(t.remove(x?x(e,y):w(e,y))):(x?(d={},d[n]=l,d=x(e,d)===x(e,y)):d=l===w(e,y),t=t||d);H=m(e,y);H=B(H)?H:"";g.push({id:x?x(e,y):p?A[D]:D,label:H,selected:d})}q||(u||null===l?a[""].unshift({id:"",label:"",selected:!t}):t||a[""].unshift({id:"?",label:"",selected:!0}));y=0;for(A=c.length;y<A;y++){d=c[y];g=a[d];
|
||||
E.length<=y?(l={element:v.clone().attr("label",d),label:g.label},s=[l],E.push(s),f.append(l.element)):(s=E[y],l=s[0],l.label!=d&&l.element.attr("label",l.label=d));H=null;D=0;for(G=g.length;D<G;D++)d=g[D],(t=s[D+1])?(H=t.element,t.label!==d.label&&H.text(t.label=d.label),t.id!==d.id&&H.val(t.id=d.id),H[0].selected!==d.selected&&(H.prop("selected",t.selected=d.selected),X&&H.prop("selected",t.selected))):(""===d.id&&u?K=u:(K=C.clone()).val(d.id).prop("selected",d.selected).attr("selected",d.selected).text(d.label),
|
||||
s.push({element:K,label:d.label,id:d.id,selected:d.selected}),H?H.after(K):l.element.append(K),H=K);for(D++;s.length>D;)s.pop().element.remove()}for(;E.length>y;)E.pop()[0].element.remove()}var l;if(!(l=t.match(d)))throw Hf("iexp",t,ta(f));var m=c(l[2]||l[1]),n=l[4]||l[6],p=l[5],r=c(l[3]||""),w=c(l[2]?l[1]:n),F=c(l[7]),x=l[8]?c(l[8]):null,E=[[{element:f,label:""}]];u&&(a(u)(e),u.removeClass("ng-scope"),u.remove());f.empty();f.on("change",function(){e.$apply(function(){var a,c=F(e)||[],d={},g,l,m,
|
||||
r,t,u,v;if(q)for(l=[],r=0,u=E.length;r<u;r++)for(a=E[r],m=1,t=a.length;m<t;m++){if((g=a[m].element)[0].selected){g=g.val();p&&(d[p]=g);if(x)for(v=0;v<c.length&&(d[n]=c[v],x(e,d)!=g);v++);else d[n]=c[g];l.push(w(e,d))}}else if(g=f.val(),"?"==g)l=s;else if(""===g)l=null;else if(x)for(v=0;v<c.length;v++){if(d[n]=c[v],x(e,d)==g){l=w(e,d);break}}else d[n]=c[g],p&&(d[p]=g),l=w(e,d);h.$setViewValue(l);k()})});h.$render=k;e.$watchCollection(F,g);q&&e.$watchCollection(function(){return h.$modelValue},g)}if(m[1]){var l=
|
||||
m[0];m=m[1];var q=h.multiple,t=h.ngOptions,u=!1,w,z=!1,C=G(Y.createElement("option")),v=G(Y.createElement("optgroup")),y=C.clone();h=0;for(var E=g.children(),D=E.length;h<D;h++)if(""===E[h].value){w=u=E.eq(h);break}l.init(m,u,y);q&&(m.$isEmpty=function(a){return!a||0===a.length});t?p(e,g,m):q?n(e,g,m):k(e,g,m,l)}}}}],Kd=["$interpolate",function(a){var c={addOption:w,removeOption:w};return{restrict:"E",priority:100,compile:function(d,e){if(F(e.value)){var f=a(d.text(),!0);f||e.$set("value",d.text())}return function(a,
|
||||
d,e){var k=d.parent(),n=k.data("$selectController")||k.parent().data("$selectController");n&&n.databound?d.prop("selected",!1):n=c;f?a.$watch(f,function(a,c){e.$set("value",a);c!==a&&n.removeOption(c);n.addOption(a,d)}):n.addOption(e.value,d);d.on("$destroy",function(){n.removeOption(e.value)})}}}}],Jd=da({restrict:"E",terminal:!1});t.angular.bootstrap?console.log("WARNING: Tried to load angular more than once."):(zd(),Bd(Ea),G(Y).ready(function(){vd(Y,gc)}))})(window,document);
|
||||
!window.angular.$$csp()&&window.angular.element(document).find("head").prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-animate){display:none !important;}ng\\:form{display:block;}</style>');
|
||||
//# sourceMappingURL=angular.min.js.map
|
@ -1,117 +0,0 @@
|
||||
/**
|
||||
* Autofill event polyfill ##version:1.0.0##
|
||||
* (c) 2014 Google, Inc.
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window) {
|
||||
var $ = window.jQuery || window.angular.element;
|
||||
var rootElement = window.document.documentElement,
|
||||
$rootElement = $(rootElement);
|
||||
|
||||
addGlobalEventListener('change', markValue);
|
||||
addValueChangeByJsListener(markValue);
|
||||
|
||||
$.prototype.checkAndTriggerAutoFillEvent = jqCheckAndTriggerAutoFillEvent;
|
||||
|
||||
// Need to use blur and not change event
|
||||
// as Chrome does not fire change events in all cases an input is changed
|
||||
// (e.g. when starting to type and then finish the input by auto filling a username)
|
||||
addGlobalEventListener('blur', function(target) {
|
||||
// setTimeout needed for Chrome as it fills other
|
||||
// form fields a little later...
|
||||
window.setTimeout(function() {
|
||||
findParentForm(target).find('input').checkAndTriggerAutoFillEvent();
|
||||
}, 20);
|
||||
});
|
||||
|
||||
window.document.addEventListener('DOMContentLoaded', function() {
|
||||
// The timeout is needed for Chrome as it auto fills
|
||||
// login forms some time after DOMContentLoaded!
|
||||
window.setTimeout(function() {
|
||||
$rootElement.find('input').checkAndTriggerAutoFillEvent();
|
||||
}, 200);
|
||||
}, false);
|
||||
|
||||
return;
|
||||
|
||||
// ----------
|
||||
|
||||
function jqCheckAndTriggerAutoFillEvent() {
|
||||
var i, el;
|
||||
for (i=0; i<this.length; i++) {
|
||||
el = this[i];
|
||||
if (!valueMarked(el)) {
|
||||
markValue(el);
|
||||
triggerChangeEvent(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function valueMarked(el) {
|
||||
var val = el.value,
|
||||
$$currentValue = el.$$currentValue;
|
||||
if (!val && !$$currentValue) {
|
||||
return true;
|
||||
}
|
||||
return val === $$currentValue;
|
||||
}
|
||||
|
||||
function markValue(el) {
|
||||
el.$$currentValue = el.value;
|
||||
}
|
||||
|
||||
function addValueChangeByJsListener(listener) {
|
||||
var jq = window.jQuery || window.angular.element,
|
||||
jqProto = jq.prototype;
|
||||
var _val = jqProto.val;
|
||||
jqProto.val = function(newValue) {
|
||||
var res = _val.apply(this, arguments);
|
||||
if (arguments.length > 0) {
|
||||
forEach(this, function(el) {
|
||||
listener(el, newValue);
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
function addGlobalEventListener(eventName, listener) {
|
||||
// Use a capturing event listener so that
|
||||
// we also get the event when it's stopped!
|
||||
// Also, the blur event does not bubble.
|
||||
rootElement.addEventListener(eventName, onEvent, true);
|
||||
|
||||
function onEvent(event) {
|
||||
var target = event.target;
|
||||
listener(target);
|
||||
}
|
||||
}
|
||||
|
||||
function findParentForm(el) {
|
||||
while (el) {
|
||||
if (el.nodeName === 'FORM') {
|
||||
return $(el);
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return $();
|
||||
}
|
||||
|
||||
function forEach(arr, listener) {
|
||||
if (arr.forEach) {
|
||||
return arr.forEach(listener);
|
||||
}
|
||||
var i;
|
||||
for (i=0; i<arr.length; i++) {
|
||||
listener(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerChangeEvent(element) {
|
||||
var doc = window.document;
|
||||
var event = doc.createEvent("HTMLEvents");
|
||||
event.initEvent("change", true, true);
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
})(window);
|
@ -1,216 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
2
syweb/webclient/js/jquery-1.8.3.min.js
vendored
13
syweb/webclient/js/jquery.peity.min.js
vendored
@ -1,13 +0,0 @@
|
||||
// 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);
|
@ -1,63 +0,0 @@
|
||||
/* ng-infinite-scroll - v1.0.0 - 2013-02-23
|
||||
Matrix: Modified to support scrolling UP to get infinite loading and to listen
|
||||
to scroll events on the PARENT element, not the window.
|
||||
*/
|
||||
var mod;
|
||||
|
||||
mod = angular.module('infinite-scroll', []);
|
||||
|
||||
mod.directive('infiniteScroll', [
|
||||
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
|
||||
return {
|
||||
link: function(scope, elem, attrs) {
|
||||
var checkWhenEnabled, handler, scrollDistance, scrollEnabled;
|
||||
$window = angular.element($window);
|
||||
scrollDistance = 0;
|
||||
if (attrs.infiniteScrollDistance != null) {
|
||||
scope.$watch(attrs.infiniteScrollDistance, function(value) {
|
||||
return scrollDistance = parseInt(value, 10);
|
||||
});
|
||||
}
|
||||
scrollEnabled = true;
|
||||
checkWhenEnabled = false;
|
||||
if (attrs.infiniteScrollDisabled != null) {
|
||||
scope.$watch(attrs.infiniteScrollDisabled, function(value) {
|
||||
scrollEnabled = !value;
|
||||
if (scrollEnabled && checkWhenEnabled) {
|
||||
checkWhenEnabled = false;
|
||||
return handler();
|
||||
}
|
||||
});
|
||||
}
|
||||
handler = function() {
|
||||
var elementTop, remaining, shouldScroll, windowTop;
|
||||
windowTop = 0;
|
||||
elementTop = elem.offset().top;
|
||||
shouldScroll = elementTop >= 0; // top of list is at the top of the window or further down the page
|
||||
if (shouldScroll && scrollEnabled) {
|
||||
if ($rootScope.$$phase) {
|
||||
return scope.$eval(attrs.infiniteScroll);
|
||||
} else {
|
||||
return scope.$apply(attrs.infiniteScroll);
|
||||
}
|
||||
} else if (shouldScroll) {
|
||||
return checkWhenEnabled = true;
|
||||
}
|
||||
};
|
||||
elem.parent().on('scroll', handler);
|
||||
scope.$on('$destroy', function() {
|
||||
return elem.parent().off('scroll', handler);
|
||||
});
|
||||
return $timeout((function() {
|
||||
if (attrs.infiniteScrollImmediateCheck) {
|
||||
if (scope.$eval(attrs.infiniteScrollImmediateCheck)) {
|
||||
return handler();
|
||||
}
|
||||
} else {
|
||||
return handler();
|
||||
}
|
||||
}), 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
angular.module('LoginController', ['matrixService'])
|
||||
.controller('LoginController', ['$scope', '$rootScope', '$location', 'matrixService', 'eventStreamService',
|
||||
function($scope, $rootScope, $location, matrixService, eventStreamService) {
|
||||
'use strict';
|
||||
|
||||
|
||||
// Assume that this is hosted on the home server, in which case the URL
|
||||
// contains the home server.
|
||||
var hs_url = $location.protocol() + "://" + $location.host();
|
||||
if ($location.port() &&
|
||||
!($location.protocol() === "http" && $location.port() === 80) &&
|
||||
!($location.protocol() === "https" && $location.port() === 443))
|
||||
{
|
||||
hs_url += ":" + $location.port();
|
||||
}
|
||||
|
||||
$scope.account = {
|
||||
homeserver: hs_url,
|
||||
desired_user_name: "",
|
||||
user_id: "",
|
||||
password: "",
|
||||
identityServer: "http://matrix.org:8090",
|
||||
pwd1: "",
|
||||
pwd2: "",
|
||||
};
|
||||
|
||||
$scope.login_types = [ "email", "mxid" ];
|
||||
$scope.login_type_label = {
|
||||
"email": "Email address",
|
||||
"mxid": "Matrix ID (e.g. @bob:matrix.org or bob)",
|
||||
};
|
||||
$scope.login_type = 'mxid'; // TODO: remember the user's preferred login_type
|
||||
|
||||
$scope.login = function() {
|
||||
matrixService.setConfig({
|
||||
homeserver: $scope.account.homeserver,
|
||||
identityServer: $scope.account.identityServer,
|
||||
});
|
||||
switch ($scope.login_type) {
|
||||
case 'mxid':
|
||||
$scope.login_with_mxid($scope.account.user_id, $scope.account.password);
|
||||
break;
|
||||
case 'email':
|
||||
matrixService.lookup3pid('email', $scope.account.user_id).then(
|
||||
function(response) {
|
||||
if (response.data['address'] == undefined) {
|
||||
$scope.login_error_msg = "Invalid email address / password";
|
||||
} else {
|
||||
console.log("Got address "+response.data['mxid']+" for email "+$scope.account.user_id);
|
||||
$scope.login_with_mxid(response.data['mxid'], $scope.account.password);
|
||||
}
|
||||
},
|
||||
function() {
|
||||
$scope.login_error_msg = "Couldn't look up email address. Is your identity server set correctly?";
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.login_with_mxid = function(mxid, password) {
|
||||
matrixService.setConfig({
|
||||
homeserver: $scope.account.homeserver,
|
||||
identityServer: $scope.account.identityServer,
|
||||
user_id: $scope.account.user_id
|
||||
});
|
||||
// try to login
|
||||
matrixService.login(mxid, password).then(
|
||||
function(response) {
|
||||
if ("access_token" in response.data) {
|
||||
$scope.feedback = "Login successful.";
|
||||
matrixService.setConfig({
|
||||
homeserver: $scope.account.homeserver,
|
||||
identityServer: $scope.account.identityServer,
|
||||
user_id: response.data.user_id,
|
||||
access_token: response.data.access_token
|
||||
});
|
||||
matrixService.saveConfig();
|
||||
$rootScope.updateHeader();
|
||||
eventStreamService.resume();
|
||||
$location.url("home");
|
||||
}
|
||||
else {
|
||||
$scope.feedback = "Failed to login: " + JSON.stringify(response.data);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
if (error.data) {
|
||||
if (error.data.errcode === "M_FORBIDDEN") {
|
||||
$scope.login_error_msg = "Incorrect username or password.";
|
||||
}
|
||||
}
|
||||
else if (error.status === 0) {
|
||||
$scope.login_error_msg = "Unable to talk to the server.";
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}]);
|
||||
|
@ -1,50 +0,0 @@
|
||||
<div ng-controller="LoginController" class="login">
|
||||
<div id="wrapper" class="loginWrapper">
|
||||
|
||||
<a href ng-click="goToPage('/')">
|
||||
<img src="img/logo.png" width="240" height="102" alt="[matrix]" style="padding: 50px"/>
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<form id="loginForm" novalidate>
|
||||
<!-- Login with an registered user -->
|
||||
<div>
|
||||
Log in using:<br/>
|
||||
|
||||
<div ng-repeat="type in login_types">
|
||||
<input type="radio" ng-model="$parent.login_type" value="{{ type }}" id="radio_{{ type }}"/>
|
||||
<label for="radio_{{ type }}">{{ login_type_label[type] }}</label>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<br/>
|
||||
<input id="user_id" size="32" type="text" ng-focus="true" ng-model="account.user_id" placeholder="{{ login_type_label[login_type] }}"/>
|
||||
<br/>
|
||||
<input id="password" size="32" type="password" ng-model="account.password" placeholder="Password"/>
|
||||
<br/><br/>
|
||||
<button id="login" ng-click="login()" ng-disabled="!account.user_id || !account.password || !account.homeserver">Login</button>
|
||||
<br/><br/>
|
||||
</div>
|
||||
|
||||
<div class="feedback">{{ feedback }} {{ login_error_msg }}</div>
|
||||
|
||||
<div id="serverConfig">
|
||||
<label for="homeserver">Home Server:</label>
|
||||
<input id="homeserver" size="32" type="text" ng-model="account.homeserver" placeholder="URL (e.g. http://matrix.org:8080)"/>
|
||||
<div class="smallPrint">Your home server stores all your conversation and account data.</div>
|
||||
<label for="identityServer">Identity Server:</label>
|
||||
<input id="identityServer" size="32" type="text" ng-model="account.identityServer" placeholder="URL (e.g. http://matrix.org:8090)"/>
|
||||
<div class="smallPrint">Matrix provides identity servers to track which emails etc. belong to which Matrix IDs.<br/>
|
||||
Only http://matrix.org:8090 currently exists.</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="#/register" style="padding-right: 0em">Create account</a>
|
||||
<a href="#/reset_password" style="display: none; ">Forgotten password?</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,190 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
angular.module('RegisterController', ['matrixService'])
|
||||
.controller('RegisterController', ['$scope', '$rootScope', '$location', 'matrixService', 'eventStreamService',
|
||||
function($scope, $rootScope, $location, matrixService, eventStreamService) {
|
||||
'use strict';
|
||||
|
||||
var config = window.webClientConfig;
|
||||
var useCaptcha = false; // default to no captcha to make it easier to get a homeserver up and running...
|
||||
if (config !== undefined) {
|
||||
useCaptcha = config.useCaptcha;
|
||||
}
|
||||
|
||||
// FIXME: factor out duplication with login-controller.js
|
||||
|
||||
// Assume that this is hosted on the home server, in which case the URL
|
||||
// contains the home server.
|
||||
var hs_url = $location.protocol() + "://" + $location.host();
|
||||
if ($location.port() &&
|
||||
!($location.protocol() === "http" && $location.port() === 80) &&
|
||||
!($location.protocol() === "https" && $location.port() === 443))
|
||||
{
|
||||
hs_url += ":" + $location.port();
|
||||
}
|
||||
|
||||
var generateClientSecret = function() {
|
||||
var ret = "";
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (var i = 0; i < 32; i++) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
$scope.account = {
|
||||
homeserver: hs_url,
|
||||
desired_user_id: "",
|
||||
desired_user_name: "",
|
||||
password: "",
|
||||
identityServer: "http://matrix.org:8090",
|
||||
pwd1: "",
|
||||
pwd2: "",
|
||||
displayName : ""
|
||||
};
|
||||
|
||||
$scope.register = function() {
|
||||
// Set the urls
|
||||
matrixService.setConfig({
|
||||
homeserver: $scope.account.homeserver,
|
||||
identityServer: $scope.account.identityServer
|
||||
});
|
||||
|
||||
if ($scope.account.pwd1 !== $scope.account.pwd2) {
|
||||
$scope.feedback = "Passwords don't match.";
|
||||
return;
|
||||
}
|
||||
else if ($scope.account.pwd1.length < 6) {
|
||||
$scope.feedback = "Password must be at least 6 characters.";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.account.email) {
|
||||
$scope.clientSecret = generateClientSecret();
|
||||
matrixService.linkEmail($scope.account.email, $scope.clientSecret, 1).then(
|
||||
function(response) {
|
||||
$scope.wait_3pid_code = true;
|
||||
$scope.sid = response.data.sid;
|
||||
$scope.feedback = "";
|
||||
},
|
||||
function(response) {
|
||||
$scope.feedback = "Couldn't request verification email!";
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$scope.registerWithMxidAndPassword($scope.account.desired_user_id, $scope.account.pwd1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.registerWithMxidAndPassword = function(mxid, password, threepidCreds) {
|
||||
matrixService.register(mxid, password, threepidCreds, useCaptcha).then(
|
||||
function(response) {
|
||||
$scope.feedback = "Success";
|
||||
if (useCaptcha) {
|
||||
Recaptcha.destroy();
|
||||
}
|
||||
// Update the current config
|
||||
var config = matrixService.config();
|
||||
angular.extend(config, {
|
||||
access_token: response.data.access_token,
|
||||
user_id: response.data.user_id
|
||||
});
|
||||
matrixService.setConfig(config);
|
||||
|
||||
// And permanently save it
|
||||
matrixService.saveConfig();
|
||||
|
||||
// Update the global scoped used_id var (used in the app header)
|
||||
$rootScope.updateHeader();
|
||||
|
||||
eventStreamService.resume();
|
||||
|
||||
if ($scope.account.displayName) {
|
||||
// FIXME: handle errors setting displayName
|
||||
matrixService.setDisplayName($scope.account.displayName);
|
||||
}
|
||||
|
||||
// Go to the user's rooms list page
|
||||
$location.url("home");
|
||||
},
|
||||
function(error) {
|
||||
console.error("Registration error: "+JSON.stringify(error));
|
||||
if (useCaptcha) {
|
||||
Recaptcha.reload();
|
||||
}
|
||||
if (error.data) {
|
||||
if (error.data.errcode === "M_USER_IN_USE") {
|
||||
$scope.feedback = "Username already taken.";
|
||||
$scope.reenter_username = true;
|
||||
}
|
||||
else if (error.data.errcode == "M_CAPTCHA_INVALID") {
|
||||
$scope.feedback = "Failed captcha.";
|
||||
}
|
||||
else if (error.data.errcode == "M_CAPTCHA_NEEDED") {
|
||||
$scope.feedback = "Captcha is required on this home " +
|
||||
"server.";
|
||||
}
|
||||
else if (error.data.error) {
|
||||
$scope.feedback = error.data.error;
|
||||
}
|
||||
}
|
||||
else if (error.status === 0) {
|
||||
$scope.feedback = "Unable to talk to the server.";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.verifyToken = function() {
|
||||
matrixService.authEmail($scope.clientSecret, $scope.sid, $scope.account.threepidtoken).then(
|
||||
function(response) {
|
||||
if (!response.data.success) {
|
||||
$scope.feedback = "Unable to verify code.";
|
||||
} else {
|
||||
$scope.registerWithMxidAndPassword($scope.account.desired_user_id, $scope.account.pwd1, [{'sid':$scope.sid, 'clientSecret':$scope.clientSecret, 'idServer': $scope.account.identityServer.split('//')[1]}]);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Unable to verify code.";
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var setupCaptcha = function() {
|
||||
console.log("Setting up ReCaptcha")
|
||||
var public_key = window.webClientConfig.recaptcha_public_key;
|
||||
if (public_key === undefined) {
|
||||
console.error("No public key defined for captcha!")
|
||||
return;
|
||||
}
|
||||
Recaptcha.create(public_key,
|
||||
"regcaptcha",
|
||||
{
|
||||
theme: "red",
|
||||
callback: Recaptcha.focus_response_field
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
if (useCaptcha) {
|
||||
setupCaptcha();
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
|
@ -1,58 +0,0 @@
|
||||
<div ng-controller="RegisterController" class="register">
|
||||
<div id="wrapper" class="loginWrapper">
|
||||
|
||||
<a href ng-click="goToPage('/')">
|
||||
<img src="img/logo.png" width="240" height="102" alt="[matrix]" style="padding: 50px"/>
|
||||
</a>
|
||||
<br/>
|
||||
|
||||
<form id="loginForm" novalidate>
|
||||
<div>
|
||||
Create account:<br/>
|
||||
|
||||
<div style="text-align: center">
|
||||
<br/>
|
||||
<input ng-show="!wait_3pid_code" id="email" size="32" type="text" ng-focus="true" ng-model="account.email" placeholder="Email address (optional)"/>
|
||||
<div ng-show="!wait_3pid_code" class="smallPrint">Specifying an email address lets other users find you on Matrix more easily,<br/>
|
||||
and will give you a way to reset your password in the future</div>
|
||||
<span ng-show="reenter_username">Choose another username:</span>
|
||||
<input ng-show="!wait_3pid_code || reenter_username" id="desired_user_id" size="32" type="text" ng-model="account.desired_user_id" placeholder="Matrix ID (e.g. bob)"/>
|
||||
<br ng-show="!wait_3pid_code" />
|
||||
<input ng-show="!wait_3pid_code" id="pwd1" size="32" type="password" ng-model="account.pwd1" placeholder="Type a password"/>
|
||||
<br ng-show="!wait_3pid_code" />
|
||||
<input ng-show="!wait_3pid_code" id="pwd2" size="32" type="password" ng-model="account.pwd2" placeholder="Confirm your password"/>
|
||||
<br ng-show="!wait_3pid_code" />
|
||||
<input ng-show="!wait_3pid_code" id="displayName" size="32" type="text" ng-model="account.displayName" placeholder="Display name (e.g. Bob Obson)"/>
|
||||
<br ng-show="!wait_3pid_code" />
|
||||
<br ng-show="!wait_3pid_code" />
|
||||
|
||||
|
||||
<div id="regcaptcha" ng-init="init()" />
|
||||
|
||||
<button ng-show="!wait_3pid_code" ng-click="register()" ng-disabled="!account.desired_user_id || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Sign up</button>
|
||||
|
||||
<div ng-show="wait_3pid_code">
|
||||
<span>Please enter the verification code sent to {{ account.email }}</span><br />
|
||||
<input id="threepidtoken" size="32" type="text" ng-focus="true" ng-model="account.threepidtoken" placeholder="Verification Code"/><br />
|
||||
<button ng-click="verifyToken()" ng-disabled="!account.threepidtoken">Validate</button>
|
||||
</div>
|
||||
<br/><br/>
|
||||
</div>
|
||||
|
||||
<div class="feedback">{{ feedback }} {{ login_error_msg }}</div>
|
||||
|
||||
<div id="serverConfig" ng-show="!wait_3pid_code">
|
||||
<label for="homeserver">Home Server:</label>
|
||||
<input id="homeserver" size="32" type="text" ng-model="account.homeserver" placeholder="URL (e.g. http://matrix.org:8080)"/>
|
||||
<div class="smallPrint">Your home server stores all your conversation and account data.</div>
|
||||
<label for="identityServer">Identity Server:</label>
|
||||
<input id="identityServer" size="32" type="text" ng-model="account.identityServer" placeholder="URL (e.g. http://matrix.org:8090)"/>
|
||||
<div class="smallPrint">Matrix provides identity servers to track which emails etc. belong to which Matrix IDs.<br/>
|
||||
Only http://matrix.org:8090 currently exists.</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,113 +0,0 @@
|
||||
/*** Mobile voodoo ***/
|
||||
|
||||
/** iPads **/
|
||||
@media all and (max-device-width: 768px) {
|
||||
#roomRecentsTableWrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/** iPhones **/
|
||||
@media all and (max-device-width: 640px) {
|
||||
|
||||
#messageTableWrapper {
|
||||
margin-right: 0px ! important;
|
||||
}
|
||||
|
||||
.leftBlock {
|
||||
width: 8em ! important;
|
||||
font-size: 8px ! important;
|
||||
}
|
||||
|
||||
.rightBlock {
|
||||
width: 0px ! important;
|
||||
display: none ! important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px ! important;
|
||||
}
|
||||
|
||||
#header {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#headerContent {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
#headerContent button {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
#messageTable,
|
||||
#wrapper,
|
||||
#controls {
|
||||
max-width: 640px ! important;
|
||||
}
|
||||
|
||||
#controls {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#headerUserId,
|
||||
#roomHeader img,
|
||||
#userIdCell,
|
||||
#roomRecentsTableWrapper,
|
||||
#usersTableWrapper,
|
||||
#controlButtons,
|
||||
.extraControls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#buttonsCell {
|
||||
width: 60px ! important;
|
||||
padding-left: 20px ! important;
|
||||
}
|
||||
|
||||
#roomLogo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
font-size: 12px ! important;
|
||||
min-height: 20px ! important;
|
||||
}
|
||||
|
||||
#roomHeader {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.roomHeaderInfo {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
#roomName {
|
||||
font-size: 12px ! important;
|
||||
margin-top: 0px ! important;
|
||||
}
|
||||
|
||||
.roomTopicSection {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#roomPage {
|
||||
top: 40px ! important;
|
||||
left: 5px ! important;
|
||||
right: 5px ! important;
|
||||
bottom: 70px ! important;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
/* stop zoom on select */
|
||||
select:focus,
|
||||
textarea,
|
||||
input
|
||||
{
|
||||
font-size: 16px ! important;
|
||||
}
|
||||
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
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', 'modelService', 'recentsService',
|
||||
function($rootScope, $scope, modelService, recentsService) {
|
||||
|
||||
// Expose the service to the view
|
||||
$scope.modelService = modelService;
|
||||
|
||||
// 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) );
|
||||
};
|
||||
|
||||
}]);
|
||||
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
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')
|
||||
.filter('orderRecents', ["matrixService", "modelService", function(matrixService, modelService) {
|
||||
return function(rooms) {
|
||||
var user_id = matrixService.config().user_id;
|
||||
|
||||
// Transform the dict into an array
|
||||
// The key, room_id, is already in value objects
|
||||
var filtered = [];
|
||||
angular.forEach(rooms, function(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 = 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 modelService
|
||||
room.recent.numUsersInRoom = modelService.getUserCountInRoom(room_id);
|
||||
|
||||
filtered.push(room);
|
||||
}
|
||||
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 latest message at first
|
||||
filtered.sort(function (roomA, roomB) {
|
||||
|
||||
var lastMsgRoomA = modelService.getLastMessage(roomA.room_id, true);
|
||||
var lastMsgRoomB = modelService.getLastMessage(roomB.room_id, true);
|
||||
|
||||
// Invite message does not have a body message nor ts
|
||||
// Puth them at the top of the list
|
||||
if (undefined === lastMsgRoomA) {
|
||||
return -1;
|
||||
}
|
||||
else if (undefined === lastMsgRoomB) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return lastMsgRoomB.origin_server_ts - lastMsgRoomA.origin_server_ts;
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
};
|
||||
}]);
|
@ -1,110 +0,0 @@
|
||||
<div ng-controller="RecentsController">
|
||||
<table class="recentsTable">
|
||||
<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.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.recent.numUsersInRoom">
|
||||
{{ room.recent.numUsersInRoom || '1' }} {{ room.recent.numUsersInRoom == 1 ? 'user' : 'users' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="recentsRoomSummaryTS">
|
||||
<!-- Use a temp var as alias to the last room message.
|
||||
Declaring it in this way ensures the data-binding -->
|
||||
{{ lastMsg = modelService.getLastMessage(room.room_id, true);"" }}
|
||||
|
||||
{{ (lastMsg.origin_server_ts) | date:'MMM d HH:mm' }}
|
||||
|
||||
<img ng-click="leave(room.room_id); $event.stopPropagation();" src="img/close.png" width="10" height="10" style="margin-bottom: -1px; margin-left: 2px;" alt="close"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="3" class="recentsRoomSummary">
|
||||
|
||||
<div ng-show="room.recent.me.content.membership === 'invite'">
|
||||
{{ room.recent.inviter | mUserDisplayName: room.room_id }} invited you
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<span ng-if="'join' === lastMsg.content.membership">
|
||||
{{ lastMsg.state_key | mUserDisplayName: room.room_id }} joined
|
||||
</span>
|
||||
<span ng-if="'leave' === lastMsg.content.membership">
|
||||
<span ng-if="lastMsg.user_id === lastMsg.state_key">
|
||||
{{lastMsg.state_key | mUserDisplayName: room.room_id }} left
|
||||
</span>
|
||||
<span ng-if="lastMsg.user_id !== lastMsg.state_key && lastMsg.prev_content">
|
||||
{{ lastMsg.user_id | mUserDisplayName: room.room_id }}
|
||||
{{ {"invite": "kicked", "join": "kicked", "ban": "unbanned"}[lastMsg.prev_content.membership] }}
|
||||
{{ lastMsg.state_key | mUserDisplayName: room.room_id }}
|
||||
</span>
|
||||
<span ng-if="lastMsg.prev_content && 'join' === lastMsg.prev_content.membership && lastMsg.content.reason">
|
||||
: {{ lastMsg.content.reason }}
|
||||
</span>
|
||||
</span>
|
||||
<span ng-if="'invite' === lastMsg.content.membership || 'ban' === lastMsg.content.membership">
|
||||
{{ lastMsg.user_id | mUserDisplayName: room.room_id }}
|
||||
{{ {"invite": "invited", "ban": "banned"}[lastMsg.content.membership] }}
|
||||
{{ lastMsg.state_key | mUserDisplayName: room.room_id }}
|
||||
<span ng-if="lastMsg.prev_content && 'ban' === lastMsg.prev_content.membership && lastMsg.content.reason">
|
||||
: {{ lastMsg.content.reason }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span ng-switch-when="displayname">
|
||||
{{ lastMsg.user_id }} changed their display name from {{ lastMsg.prev_content.displayname }} to {{ lastMsg.content.displayname }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.room.message">
|
||||
<div ng-switch="lastMsg.content.msgtype">
|
||||
<div ng-switch-when="m.text">
|
||||
{{ lastMsg.user_id | mUserDisplayName: room.room_id }} :
|
||||
<span ng-bind-html="(lastMsg.content.body) | linky:'_blank'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.image">
|
||||
{{ lastMsg.user_id | mUserDisplayName: room.room_id }} sent an image
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.emote">
|
||||
<span ng-bind-html="'* ' + (lastMsg.user_id | mUserDisplayName: room.room_id) + ' ' + lastMsg.content.body | linky:'_blank'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-switch-default>
|
||||
{{ lastMsg.content.body | linky:'_blank' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.room.topic">
|
||||
{{ lastMsg.user_id | mUserDisplayName: room.room_id }} changed the topic to: {{ lastMsg.content.topic }}
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.room.name">
|
||||
{{ lastMsg.user_id | mUserDisplayName: room.room_id }} changed the room name to: {{ lastMsg.content.name }}
|
||||
</div>
|
||||
|
||||
<div ng-switch-default>
|
||||
<div ng-if="lastMsg.type.indexOf('m.call.') === 0">
|
||||
Call
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -1,581 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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', 'mUserDisplayNameFilter',
|
||||
function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, modelService, recentsService, commandsService, mUserDisplayNameFilter) {
|
||||
'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;
|
||||
|
||||
$scope.state = {
|
||||
user_id: matrixService.config().user_id,
|
||||
permission_denied: undefined, // If defined, this string contains the reason why the user cannot join the room
|
||||
first_pagination: true, // this is toggled off when the first pagination is done
|
||||
can_paginate: false, // this is toggled off when we are not ready yet to paginate or when we run out of items
|
||||
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
|
||||
stream_failure: undefined, // the response when the stream fails
|
||||
waiting_for_joined_event: false, // true when the join request is pending. Back to false once the corresponding m.room.member event is received
|
||||
messages_visibility: "hidden", // In order to avoid flickering when scrolling down the message table at the page opening, delay the message table display
|
||||
};
|
||||
|
||||
$scope.imageURLToSend = "";
|
||||
|
||||
|
||||
// vars and functions for updating the name
|
||||
$scope.name = {
|
||||
isEditing: false,
|
||||
newNameText: "",
|
||||
editName: function() {
|
||||
if ($scope.name.isEditing) {
|
||||
console.log("Warning: Already editing name.");
|
||||
return;
|
||||
};
|
||||
|
||||
var nameEvent = $scope.room.current_room_state.state_events['m.room.name'];
|
||||
if (nameEvent) {
|
||||
$scope.name.newNameText = nameEvent.content.name;
|
||||
}
|
||||
else {
|
||||
$scope.name.newNameText = "";
|
||||
}
|
||||
|
||||
// Force focus to the input
|
||||
$timeout(function() {
|
||||
angular.element('.roomNameInput').focus();
|
||||
}, 0);
|
||||
|
||||
$scope.name.isEditing = true;
|
||||
},
|
||||
updateName: function() {
|
||||
console.log("Updating name to "+$scope.name.newNameText);
|
||||
matrixService.setName($scope.room_id, $scope.name.newNameText).then(
|
||||
function() {
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Request failed: " + error.data.error;
|
||||
}
|
||||
);
|
||||
|
||||
$scope.name.isEditing = false;
|
||||
},
|
||||
cancelEdit: function() {
|
||||
$scope.name.isEditing = false;
|
||||
}
|
||||
};
|
||||
|
||||
// vars and functions for updating the topic
|
||||
$scope.topic = {
|
||||
isEditing: false,
|
||||
newTopicText: "",
|
||||
editTopic: function() {
|
||||
if ($scope.topic.isEditing) {
|
||||
console.log("Warning: Already editing topic.");
|
||||
return;
|
||||
}
|
||||
var topicEvent = $scope.room.current_room_state.state_events['m.room.topic'];
|
||||
if (topicEvent) {
|
||||
$scope.topic.newTopicText = topicEvent.content.topic;
|
||||
}
|
||||
else {
|
||||
$scope.topic.newTopicText = "";
|
||||
}
|
||||
|
||||
// Force focus to the input
|
||||
$timeout(function() {
|
||||
angular.element('.roomTopicInput').focus();
|
||||
}, 0);
|
||||
|
||||
$scope.topic.isEditing = true;
|
||||
},
|
||||
updateTopic: function() {
|
||||
console.log("Updating topic to "+$scope.topic.newTopicText);
|
||||
matrixService.setTopic($scope.room_id, $scope.topic.newTopicText).then(
|
||||
function() {
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Request failed: " + error.data.error;
|
||||
}
|
||||
);
|
||||
|
||||
$scope.topic.isEditing = false;
|
||||
},
|
||||
cancelEdit: function() {
|
||||
$scope.topic.isEditing = false;
|
||||
}
|
||||
};
|
||||
|
||||
var scrollToBottom = function(force) {
|
||||
console.log("Scrolling to bottom");
|
||||
|
||||
// Do not autoscroll to the bottom to display the new event if the user is not at the bottom.
|
||||
// Exception: in case where the event is from the user, we want to force scroll to the bottom
|
||||
var objDiv = document.getElementById("messageTableWrapper");
|
||||
// add a 10px buffer to this check so if the message list is not *quite*
|
||||
// at the bottom it still scrolls since it basically is at the bottom.
|
||||
if ((10 + objDiv.offsetHeight + objDiv.scrollTop >= objDiv.scrollHeight) || force) {
|
||||
|
||||
$timeout(function() {
|
||||
objDiv.scrollTop = objDiv.scrollHeight;
|
||||
|
||||
// Show the message table once the first scrolldown is done
|
||||
if ("visible" !== $scope.state.messages_visibility) {
|
||||
$timeout(function() {
|
||||
$scope.state.messages_visibility = "visible";
|
||||
}, 0);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
||||
if (isLive && event.room_id === $scope.room_id) {
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||
// if there is a live event affecting us
|
||||
if (isLive && event.room_id === $scope.room_id && event.state_key === $scope.state.user_id) {
|
||||
// if someone else changed our state..
|
||||
if (event.user_id !== $scope.state.user_id && "invite" !== event.content.membership && "join" !== event.content.membership) {
|
||||
if ("ban" === event.content.membership) {
|
||||
$scope.state.permission_denied = "You have been banned by " + mUserDisplayNameFilter(event.user_id);
|
||||
}
|
||||
else {
|
||||
$scope.state.permission_denied = "You have been kicked by " + mUserDisplayNameFilter(event.user_id);
|
||||
}
|
||||
}
|
||||
else {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.memberCount = function() {
|
||||
return Object.keys($scope.room.now.members).length;
|
||||
};
|
||||
|
||||
$scope.paginateMore = function() {
|
||||
if ($scope.state.can_paginate) {
|
||||
// console.log("Paginating more.");
|
||||
paginate(MESSAGES_PER_PAGINATION);
|
||||
}
|
||||
};
|
||||
|
||||
var paginate = function(numItems) {
|
||||
//console.log("paginate " + numItems + " and first_pagination is " + $scope.state.first_pagination);
|
||||
if ($scope.state.paginating || !$scope.room_id) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
$scope.state.paginating = true;
|
||||
}
|
||||
|
||||
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, $scope.room.old_room_state.pagination_token, numItems).then(
|
||||
function(response) {
|
||||
|
||||
eventHandlerService.handleRoomMessages($scope.room_id, response.data, false, 'b');
|
||||
if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
|
||||
// no more messages to paginate. this currently never gets turned true again, as we never
|
||||
// expire paginated contents in the current implementation.
|
||||
$scope.state.can_paginate = false;
|
||||
}
|
||||
|
||||
$scope.state.paginating = false;
|
||||
|
||||
var wrapper = $("#messageTableWrapper")[0];
|
||||
var table = $("#messageTable")[0];
|
||||
// console.log("wrapper height=" + wrapper.clientHeight + ", table scrollHeight=" + table.scrollHeight);
|
||||
|
||||
if ($scope.state.can_paginate) {
|
||||
// check we don't have to pull in more messages
|
||||
// n.b. we dispatch through a timeout() to allow the digest to run otherwise the .height methods are stale
|
||||
$timeout(function() {
|
||||
if (table.scrollHeight < wrapper.clientHeight) {
|
||||
paginate(MESSAGES_PER_PAGINATION);
|
||||
scrollToBottom();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if ($scope.state.first_pagination) {
|
||||
scrollToBottom(true);
|
||||
$scope.state.first_pagination = false;
|
||||
}
|
||||
else {
|
||||
// lock the scroll position
|
||||
$timeout(function() {
|
||||
// FIXME: this risks a flicker before the scrollTop is actually updated, but we have to
|
||||
// dispatch it into a function in order to first update the layout. The right solution
|
||||
// might be to implement it as a directive, more like
|
||||
// http://stackoverflow.com/questions/23736647/how-to-retain-scroll-position-of-ng-repeat-in-angularjs
|
||||
// however, this specific solution breaks because it measures the rows height before
|
||||
// the contents are interpolated.
|
||||
wrapper.scrollTop = originalTopRow ? (originalTopRow.offsetTop + wrapper.scrollTop) : 0;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
console.log("Failed to paginateBackMessages: " + JSON.stringify(error));
|
||||
$scope.state.paginating = false;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var updatePresenceTimes = function() {
|
||||
$scope.now = new Date().getTime();
|
||||
// TODO: don't bother polling every 5s if we know none of our counters are younger than 1 minute
|
||||
$timeout(updatePresenceTimes, 5 * 1000);
|
||||
};
|
||||
|
||||
$scope.send = function() {
|
||||
var input = $('#mainInput').val();
|
||||
|
||||
if (undefined === input || input === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToBottom(true);
|
||||
|
||||
// Store the command in the history
|
||||
$rootScope.$broadcast("commandHistory:BROADCAST_NEW_HISTORY_ITEM(item)",
|
||||
input);
|
||||
|
||||
var isEmote = input.indexOf("/me ") === 0;
|
||||
var promise;
|
||||
if (!isEmote) {
|
||||
promise = commandsService.processInput($scope.room_id, input);
|
||||
}
|
||||
var echo = false;
|
||||
|
||||
|
||||
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) {
|
||||
// Echo the message to the room
|
||||
// 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: (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,
|
||||
type: "m.room.message",
|
||||
user_id: $scope.state.user_id,
|
||||
echo_msg_state: "messagePending" // Add custom field to indicate the state of this fake message to HTML
|
||||
};
|
||||
|
||||
$('#mainInput').val('');
|
||||
$scope.room.addMessageEvent(echoMessage);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
// Reset previous feedback
|
||||
$scope.feedback = "";
|
||||
|
||||
promise.then(
|
||||
function(response) {
|
||||
console.log("Request successfully sent");
|
||||
|
||||
if (echo) {
|
||||
// Mark this fake message event with its allocated event_id
|
||||
// When the true message event will come from the events stream (in handleMessage),
|
||||
// we will be able to replace the fake one by the true one
|
||||
echoMessage.event_id = response.data.event_id;
|
||||
}
|
||||
else {
|
||||
$('#mainInput').val('');
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = error.data.error;
|
||||
|
||||
if (echoMessage) {
|
||||
// Mark the message as unsent for the rest of the page life
|
||||
echoMessage.origin_server_ts = "Unsent";
|
||||
echoMessage.echo_msg_state = "messageUnSent";
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Tries to find a suitable room ID for this room.
|
||||
$scope.onInit = function() {
|
||||
console.log("onInit");
|
||||
|
||||
// Extract the room identifier being loaded
|
||||
var room_id_or_alias;
|
||||
if ($routeParams.room_id_or_alias) { // provided in the url
|
||||
room_id_or_alias = decodeURIComponent($routeParams.room_id_or_alias);
|
||||
}
|
||||
|
||||
eventHandlerService.joinRoom(room_id_or_alias).then(function(roomId) {
|
||||
$scope.room_id = roomId;
|
||||
$scope.room = modelService.getRoom($scope.room_id);
|
||||
|
||||
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)) {
|
||||
// If we just joined a room, we won't have this history from initial sync, so we should try to paginate it anyway
|
||||
$scope.state.first_pagination = true;
|
||||
}
|
||||
else {
|
||||
// There is no need to do a 1st pagination (initialSync provided enough to fill a page)
|
||||
$scope.state.first_pagination = false;
|
||||
}
|
||||
|
||||
// Make recents highlight the current room
|
||||
recentsService.setSelectedRoomId($scope.room_id);
|
||||
|
||||
updatePresenceTimes();
|
||||
|
||||
// Allow pagination
|
||||
$scope.state.can_paginate = true;
|
||||
|
||||
// Do a first pagination only if it is required (e.g. we've JUST joined a room and have no messages to display.)
|
||||
// FIXME: Should be no more require when initialSync/{room_id} will be available
|
||||
if ($scope.state.first_pagination) {
|
||||
paginate(MESSAGES_PER_PAGINATION);
|
||||
}
|
||||
|
||||
// Scroll down as soon as possible so that we point to the last message
|
||||
// if it already exists in memory
|
||||
scrollToBottom(true);
|
||||
},
|
||||
function(err) {
|
||||
if (err.data.errcode === "M_FORBIDDEN") {
|
||||
$scope.state.permission_denied = "You do not have permission to join this room";
|
||||
}
|
||||
else {
|
||||
console.log("Error: cannot join room: "+JSON.stringify(err));
|
||||
$location.url("/");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.leaveRoom = function() {
|
||||
|
||||
matrixService.leave($scope.room_id).then(
|
||||
function(response) {
|
||||
console.log("Left room " + $scope.room_id);
|
||||
$location.url("home");
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failed to leave room: " + error.data.error;
|
||||
});
|
||||
};
|
||||
|
||||
// used to send an image based on just a URL, rather than uploading one
|
||||
$scope.sendImage = function(url, body) {
|
||||
scrollToBottom(true);
|
||||
|
||||
matrixService.sendImageMessage($scope.room_id, url, body).then(
|
||||
function() {
|
||||
console.log("Image sent");
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failed to send image: " + error.data.error;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.fileToSend;
|
||||
$scope.$watch("fileToSend", function(newValue, oldValue) {
|
||||
if ($scope.fileToSend) {
|
||||
// Upload this file
|
||||
mFileUpload.uploadFileAndThumbnail($scope.fileToSend, THUMBNAIL_SIZE).then(
|
||||
function(fileMessage) {
|
||||
// fileMessage is complete message structure, send it as is
|
||||
matrixService.sendMessage($scope.room_id, undefined, fileMessage).then(
|
||||
function() {
|
||||
console.log("File message sent");
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failed to send file message: " + error.data.error;
|
||||
});
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't upload file";
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.loadMoreHistory = function() {
|
||||
paginate(MESSAGES_PER_PAGINATION);
|
||||
};
|
||||
|
||||
$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.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.localVideoSelector = '#localVideo';
|
||||
call.remoteVideoSelector = '#remoteVideo';
|
||||
call.placeVideoCall();
|
||||
$rootScope.currentCall = call;
|
||||
};
|
||||
|
||||
$scope.openJson = function(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 = modelService.getUserPowerLevel;
|
||||
|
||||
var modalInstance = $modal.open({
|
||||
templateUrl: 'eventInfoTemplate.html',
|
||||
controller: 'EventInfoController',
|
||||
scope: $scope
|
||||
});
|
||||
|
||||
modalInstance.result.then(function(action) {
|
||||
if (action === "redact") {
|
||||
var eventId = $scope.event_selected.event_id;
|
||||
console.log("Redacting event ID " + eventId);
|
||||
matrixService.redactEvent(
|
||||
$scope.event_selected.room_id,
|
||||
eventId
|
||||
).then(function(response) {
|
||||
console.log("Redaction = " + JSON.stringify(response));
|
||||
}, function(error) {
|
||||
console.error("Failed to redact event: "+JSON.stringify(error));
|
||||
if (error.data.error) {
|
||||
$scope.feedback = error.data.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, function() {
|
||||
// any dismiss code
|
||||
});
|
||||
};
|
||||
|
||||
$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.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;
|
||||
|
||||
});
|
@ -1,276 +0,0 @@
|
||||
/*
|
||||
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('RoomController')
|
||||
// XXX FIXME : This has tight coupling with $scope.room.now.members
|
||||
.directive('tabComplete', ['$timeout', function ($timeout) {
|
||||
return function (scope, element, attrs) {
|
||||
element.bind("keydown keypress", function (event) {
|
||||
// console.log("event: " + event.which);
|
||||
var TAB = 9;
|
||||
var SHIFT = 16;
|
||||
var keypressCode = event.which;
|
||||
if (keypressCode === TAB) {
|
||||
if (!scope.tabCompleting) { // cache our starting text
|
||||
scope.tabCompleteOriginal = element[0].value;
|
||||
scope.tabCompleting = true;
|
||||
scope.tabCompleteIndex = 0;
|
||||
}
|
||||
|
||||
// loop in the right direction
|
||||
if (event.shiftKey) {
|
||||
scope.tabCompleteIndex--;
|
||||
if (scope.tabCompleteIndex < 0) {
|
||||
// wrap to the last search match, and fix up to a real
|
||||
// index value after we've matched
|
||||
scope.tabCompleteIndex = Number.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
scope.tabCompleteIndex++;
|
||||
}
|
||||
|
||||
|
||||
var searchIndex = 0;
|
||||
var targetIndex = scope.tabCompleteIndex;
|
||||
var text = scope.tabCompleteOriginal;
|
||||
|
||||
// console.log("targetIndex: " + targetIndex + ",
|
||||
// text=" + text);
|
||||
|
||||
// FIXME: use the correct regexp to recognise userIDs --M
|
||||
//
|
||||
// XXX: I don't really know what the point of this is. You
|
||||
// WANT to match freeform text given you want to match display
|
||||
// names AND user IDs. Surely you just want to get the last
|
||||
// word out of the input text and that's that?
|
||||
// Am I missing something here? -- Kegan
|
||||
//
|
||||
// You're not missing anything - my point was that we should
|
||||
// explicitly define the syntax for user IDs /somewhere/.
|
||||
// Meanwhile as long as the delimeters are well defined, we
|
||||
// could just pick "the last word". But to know what the
|
||||
// correct delimeters are, we probably do need a formal
|
||||
// syntax for user IDs to refer to... --Matthew
|
||||
|
||||
var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
|
||||
|
||||
if (targetIndex === 0) { // 0 is always the original text
|
||||
element[0].value = text;
|
||||
// Force angular to wake up and update the input ng-model
|
||||
// by firing up input event
|
||||
angular.element(element[0]).triggerHandler('input');
|
||||
}
|
||||
else if (search && search[1]) {
|
||||
// console.log("search found: " + search+" from "+text);
|
||||
var expansion;
|
||||
|
||||
// FIXME: could do better than linear search here
|
||||
angular.forEach(scope.room.now.members, function(item, name) {
|
||||
if (item.event.content.displayname && searchIndex < targetIndex) {
|
||||
if (item.event.content.displayname.toLowerCase().indexOf(search[1].toLowerCase()) === 0) {
|
||||
expansion = item.event.content.displayname;
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (searchIndex < targetIndex) { // then search raw mxids
|
||||
angular.forEach(scope.room.now.members, function(item, name) {
|
||||
if (searchIndex < targetIndex) {
|
||||
// === 1 because mxids are @username
|
||||
if (name.toLowerCase().indexOf(search[1].toLowerCase()) === 1) {
|
||||
expansion = name;
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchIndex === targetIndex ||
|
||||
targetIndex === Number.MAX_VALUE) {
|
||||
// xchat-style tab complete, add a colon if tab
|
||||
// completing at the start of the text
|
||||
if (search[0].length === text.length)
|
||||
expansion += ": ";
|
||||
else
|
||||
expansion += " ";
|
||||
element[0].value = text.replace(/@?([a-zA-Z0-9_\-:\.]+)$/, expansion);
|
||||
// cancel blink
|
||||
element[0].className = "";
|
||||
if (targetIndex === Number.MAX_VALUE) {
|
||||
// wrap the index around to the last index found
|
||||
scope.tabCompleteIndex = searchIndex;
|
||||
targetIndex = searchIndex;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// console.log("wrapped!");
|
||||
element[0].className = "blink"; // XXX: slightly naughty to bypass angular
|
||||
$timeout(function() {
|
||||
element[0].className = "";
|
||||
}, 150);
|
||||
element[0].value = text;
|
||||
scope.tabCompleteIndex = 0;
|
||||
}
|
||||
|
||||
// Force angular to wak up and update the input ng-model by
|
||||
// firing up input event
|
||||
angular.element(element[0]).triggerHandler('input');
|
||||
}
|
||||
else {
|
||||
scope.tabCompleteIndex = 0;
|
||||
}
|
||||
// prevent the default TAB operation (typically focus shifting)
|
||||
event.preventDefault();
|
||||
}
|
||||
else if (keypressCode !== SHIFT && scope.tabCompleting) {
|
||||
scope.tabCompleting = false;
|
||||
scope.tabCompleteIndex = 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
}])
|
||||
// A directive which stores text sent into it and restores it via up/down arrows
|
||||
.directive('commandHistory', [ function() {
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
// 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);
|
||||
},
|
||||
|
||||
}
|
||||
}])
|
||||
|
||||
// A directive to anchor the scroller position at the bottom when the browser is resizing.
|
||||
// When the screen resizes, the bottom of the element remains the same, not the top.
|
||||
.directive('keepScroll', ['$window', function($window) {
|
||||
return {
|
||||
link: function(scope, elem, attrs) {
|
||||
|
||||
scope.windowHeight = $window.innerHeight;
|
||||
|
||||
// Listen to window size change
|
||||
angular.element($window).bind('resize', function() {
|
||||
|
||||
// If the scroller is scrolled to the bottom, there is nothing to do.
|
||||
// The browser will move it as expected
|
||||
if (elem.scrollTop() + elem.height() !== elem[0].scrollHeight) {
|
||||
// Else, move the scroller position according to the window height change delta
|
||||
var windowHeightDelta = $window.innerHeight - scope.windowHeight;
|
||||
elem.scrollTop(elem.scrollTop() - windowHeightDelta);
|
||||
}
|
||||
|
||||
// Store the new window height for the next screen size change
|
||||
scope.windowHeight = $window.innerHeight;
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
@ -1,282 +0,0 @@
|
||||
<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 room.now.members | orderMembersList" class="userAvatar">
|
||||
<div class="userAvatarFrame" ng-class="(member.user.event.content.presence === 'online' ? 'online' : (member.user.event.content.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.event.content.membership == 'invite' ? 'invited' : '')">
|
||||
<img class="userAvatarImage mouse-pointer"
|
||||
ng-click="$parent.goToUserPage(member.id)"
|
||||
ng-src="{{member.user.event.content.avatar_url || 'img/default-profile.png'}}"
|
||||
alt="{{ member.user.event.content.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
|
||||
title="{{ member.id }} - power: {{ member.power_level }}"
|
||||
width="80" height="80"/>
|
||||
<!-- <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div> -->
|
||||
</div>
|
||||
<div class="userName">
|
||||
<pie-chart ng-show="member.power_level_norm" data="[ (member.power_level_norm + 0), (100 - member.power_level_norm) ]"></pie-chart>
|
||||
{{ member.id | mUserDisplayName:room_id:true }}
|
||||
<span ng-show="member.user.event.content.last_active_ago" style="color: #aaa">({{ member.user.event.content.last_active_ago + (now - member.user.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">
|
||||
<!-- room.now.members[msg.user_id].user.event.content.avatar_url is just backwards compat, and can be removed in the future. Synapse didn't used to
|
||||
send m.room.member updates when avatar urls changed, so the image which should be visible here just going off room state isn't visible. We fix
|
||||
this by reading off the m.presence url -->
|
||||
<img class="avatarImage" ng-src="{{ msg.__room_member.cnt.avatar_url || msg.__room_member.content.avatar_url || room.now.members[msg.user_id].user.event.content.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 || room.now.members[msg.state_key].user.event.content.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 || room.now.members[msg.state_key].user.event.content.displayname || msg.state_key }} left
|
||||
</span>
|
||||
<span ng-if="msg.user_id !== msg.state_key && msg.prev_content">
|
||||
{{ msg.content.displayname || room.now.members[msg.user_id].user.event.content.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.type === "m.room.message"' ng-switch='msg.content.msgtype'>
|
||||
<span ng-switch-when="m.emote"
|
||||
ng-class="containsBingWord(msg) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
|
||||
ng-bind-html="'* ' + (msg.__room_member.cnt.displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"
|
||||
/>
|
||||
|
||||
<span ng-switch-when="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.format === 'org.matrix.custom.html') ? (msg.content.formatted_body | unsanitizedLinky) : (msg.content.body | linky:'_blank') "/>
|
||||
|
||||
<div ng-switch-when="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.$parent.fullScreenImageURL = msg.content.url; $event.stopPropagation();"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span ng-switch-when="m.file" ng-class="msg.echo_msg_state">
|
||||
<a href="{{ msg.content.url}}" target="_blank">{{ msg.content.body }}</a>
|
||||
<div ng-show='msg.content.thumbnail_url' ng-style="{ 'height' : msg.content.thumbnail_info.h }">
|
||||
<a href="{{ msg.content.url}}" target="_blank">
|
||||
<img class="image mouse-pointer" ng-src="{{ msg.content.thumbnail_url }}"/>
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span ng-switch-default
|
||||
ng-class="containsBingWord(msg) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
|
||||
ng-bind-html="msg.content.body | linky:'_blank'"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<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>
|
||||
|
||||
<span ng-if="'m.room.topic' === msg.type">
|
||||
{{ msg.__room_member.cnt.displayname || msg.user_id }} changed the topic to: {{ msg.content.topic }}
|
||||
</span>
|
||||
|
||||
<span ng-if="'m.room.name' === msg.type">
|
||||
{{ msg.__room_member.cnt.displayname || msg.user_id }} changed the room name to: {{ msg.content.name }}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td class="rightBlock">
|
||||
<img class="avatarImage" ng-src="{{ room.now.members[msg.user_id].user.event.content.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="fileToSend" 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>
|
@ -1,221 +0,0 @@
|
||||
/*
|
||||
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('SettingsController', ['matrixService', 'mFileUpload', 'mFileInput'])
|
||||
.controller('SettingsController', ['$scope', 'matrixService', 'mFileUpload',
|
||||
function($scope, matrixService, mFileUpload) {
|
||||
// XXX: duplicated from register
|
||||
var generateClientSecret = function() {
|
||||
var ret = "";
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (var i = 0; i < 32; i++) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
$scope.config = matrixService.config();
|
||||
|
||||
$scope.profile = {
|
||||
displayName: "",
|
||||
avatarUrl: ""
|
||||
};
|
||||
|
||||
// The profile as stored on the server
|
||||
$scope.profileOnServer = {
|
||||
displayName: "",
|
||||
avatarUrl: ""
|
||||
};
|
||||
|
||||
$scope.onInit = function() {
|
||||
// Load profile data
|
||||
// Display name
|
||||
matrixService.getDisplayName($scope.config.user_id).then(
|
||||
function(response) {
|
||||
$scope.profile.displayName = response.data.displayname;
|
||||
$scope.profileOnServer.displayName = response.data.displayname;
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't load display name";
|
||||
}
|
||||
);
|
||||
// Avatar
|
||||
matrixService.getProfilePictureUrl($scope.config.user_id).then(
|
||||
function(response) {
|
||||
$scope.profile.avatarUrl = response.data.avatar_url;
|
||||
$scope.profileOnServer.avatarUrl = response.data.avatar_url;
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't load avatar URL";
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.$watch("profile.avatarFile", function(newValue, oldValue) {
|
||||
if ($scope.profile.avatarFile) {
|
||||
console.log("Uploading new avatar file...");
|
||||
mFileUpload.uploadFile($scope.profile.avatarFile).then(
|
||||
function(url) {
|
||||
$scope.profile.avatarUrl = url;
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't upload image";
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.saveProfile = function() {
|
||||
if ($scope.profile.displayName !== $scope.profileOnServer.displayName) {
|
||||
setDisplayName($scope.profile.displayName);
|
||||
}
|
||||
if ($scope.profile.avatarUrl !== $scope.profileOnServer.avatarUrl) {
|
||||
setAvatar($scope.profile.avatarUrl);
|
||||
}
|
||||
};
|
||||
|
||||
var setDisplayName = function(displayName) {
|
||||
matrixService.setDisplayName(displayName).then(
|
||||
function(response) {
|
||||
$scope.feedback = "Updated display name.";
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't update display name: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var setAvatar = function(avatarURL) {
|
||||
console.log("Updating avatar to " + avatarURL);
|
||||
matrixService.setProfilePictureUrl(avatarURL).then(
|
||||
function(response) {
|
||||
console.log("Updated avatar");
|
||||
$scope.feedback = "Updated avatar.";
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't update avatar: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.linkedEmails = {
|
||||
linkNewEmail: "", // the email entry box
|
||||
emailBeingAuthed: undefined, // to populate verification text
|
||||
authSid: undefined, // the token id from the IS
|
||||
emailCode: "", // the code entry box
|
||||
linkedEmailList: matrixService.config().emailList // linked email list
|
||||
};
|
||||
|
||||
$scope.linkEmail = function(email) {
|
||||
if (email != $scope.linkedEmails.emailBeingAuthed) {
|
||||
$scope.linkedEmails.emailBeingAuthed = email;
|
||||
$scope.clientSecret = generateClientSecret();
|
||||
$scope.sendAttempt = 0;
|
||||
}
|
||||
$scope.sendAttempt++;
|
||||
matrixService.linkEmail(email, $scope.clientSecret, $scope.sendAttempt).then(
|
||||
function(response) {
|
||||
if (response.data.success === true) {
|
||||
$scope.linkedEmails.authSid = response.data.sid;
|
||||
$scope.emailFeedback = "You have been sent an email.";
|
||||
$scope.linkedEmails.emailBeingAuthed = email;
|
||||
}
|
||||
else {
|
||||
$scope.emailFeedback = "Failed to send email.";
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.emailFeedback = "Can't send email: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.submitEmailCode = function() {
|
||||
var tokenId = $scope.linkedEmails.authSid;
|
||||
if (tokenId === undefined) {
|
||||
$scope.emailFeedback = "You have not requested a code with this email.";
|
||||
return;
|
||||
}
|
||||
matrixService.authEmail($scope.clientSecret, $scope.linkedEmails.authSid, $scope.linkedEmails.emailCode).then(
|
||||
function(response) {
|
||||
if ("errcode" in response.data) {
|
||||
$scope.emailFeedback = "Failed to authenticate email.";
|
||||
return;
|
||||
}
|
||||
matrixService.bindEmail(matrixService.config().user_id, tokenId, $scope.clientSecret).then(
|
||||
function(response) {
|
||||
if ('errcode' in response.data) {
|
||||
$scope.emailFeedback = "Failed to link email.";
|
||||
return;
|
||||
}
|
||||
var config = matrixService.config();
|
||||
var emailList = {};
|
||||
if ("emailList" in config) {
|
||||
emailList = config.emailList;
|
||||
}
|
||||
emailList[$scope.linkedEmails.emailBeingAuthed] = response;
|
||||
// save the new email list
|
||||
config.emailList = emailList;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
// invalidate the email being authed and update UI.
|
||||
$scope.linkedEmails.emailBeingAuthed = undefined;
|
||||
$scope.emailFeedback = "";
|
||||
$scope.linkedEmails.linkedEmailList = emailList;
|
||||
$scope.linkedEmails.linkNewEmail = "";
|
||||
$scope.linkedEmails.emailCode = "";
|
||||
}, function(reason) {
|
||||
$scope.emailFeedback = "Failed to link email: " + reason;
|
||||
}
|
||||
);
|
||||
},
|
||||
function(reason) {
|
||||
$scope.emailFeedback = "Failed to auth email: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/*** Desktop notifications section ***/
|
||||
$scope.settings = {
|
||||
notifications: undefined,
|
||||
bingWords: matrixService.config().bingWords
|
||||
};
|
||||
|
||||
$scope.saveBingWords = function() {
|
||||
console.log("Saving words: "+JSON.stringify($scope.settings.bingWords));
|
||||
var config = matrixService.config();
|
||||
config.bingWords = $scope.settings.bingWords;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
};
|
||||
|
||||
// If the browser supports it, check the desktop notification state
|
||||
if ("Notification" in window) {
|
||||
$scope.settings.notifications = window.Notification.permission;
|
||||
}
|
||||
|
||||
$scope.requestNotifications = function() {
|
||||
console.log("requestNotifications");
|
||||
window.Notification.requestPermission(function (permission) {
|
||||
console.log(" -> User decision: " + permission);
|
||||
$scope.settings.notifications = permission;
|
||||
});
|
||||
};
|
||||
}]);
|
@ -1,106 +0,0 @@
|
||||
<div ng-controller="SettingsController" class="user" data-ng-init="onInit()">
|
||||
|
||||
<div id="wrapper">
|
||||
|
||||
<div id="genericHeading">
|
||||
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
|
||||
</div>
|
||||
|
||||
<h1>Settings</h1>
|
||||
<div class="section">
|
||||
<form>
|
||||
<div class="profile-avatar">
|
||||
<img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}" m-file-input="profile.avatarFile"/>
|
||||
</div>
|
||||
<div>
|
||||
<input id="user-displayname-input" size="40" ng-model="profile.displayName" placeholder="Your display name"/>
|
||||
<br/>
|
||||
<button id="user-save-button"
|
||||
ng-disabled="(profile.displayName === profileOnServer.displayName) && (profile.avatarUrl === profileOnServer.avatarUrl)"
|
||||
ng-click="saveProfile()">Save changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Linked emails</h3>
|
||||
<div class="section">
|
||||
<form>
|
||||
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
|
||||
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
|
||||
Link Email
|
||||
</button>
|
||||
{{ emailFeedback }}
|
||||
</form>
|
||||
<form ng-hide="!linkedEmails.emailBeingAuthed">
|
||||
Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
|
||||
<br />
|
||||
<input size="20" ng-model="linkedEmails.emailCode" ng-enter="submitEmailCode(linkedEmails.emailCode)" />
|
||||
<button ng-disabled="!linkedEmails.emailCode || !linkedEmails.linkNewEmail" ng-click="submitEmailCode(linkedEmails.emailCode)">
|
||||
Submit Code
|
||||
</button>
|
||||
</form>
|
||||
<table>
|
||||
<tr ng-repeat="(address, info) in linkedEmails.linkedEmailList">
|
||||
<td>{{address}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Desktop notifications</h3>
|
||||
<div class="section" ng-switch="settings.notifications">
|
||||
<div ng-switch-when="granted">
|
||||
Notifications are enabled.
|
||||
<div class="section">
|
||||
<h4>Specific words to alert on:</h4>
|
||||
<p>If blank, all messages will trigger an alert. Your username & display name always alerts.</p>
|
||||
<input size=40 name="bingWords" ng-model="settings.bingWords" ng-list placeholder="Enter words separated with , (supports regex)"
|
||||
ng-blur="saveBingWords()"/>
|
||||
<ul>
|
||||
<li ng-repeat="word in settings.bingWords">{{word}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="denied">
|
||||
You have denied permission for notifications.<br/>
|
||||
To enable it, reset the notification setting for this web site into your browser settings.
|
||||
</div>
|
||||
<div ng-switch-when="default">
|
||||
<button ng-click="requestNotifications()" style="font-size: 14pt">Enable desktop notifications</button>
|
||||
</div>
|
||||
<div ng-switch-default="">
|
||||
Sorry, your browser does not support notifications.
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Configuration</h3>
|
||||
<div class="section">
|
||||
<div>Home server: {{ config.homeserver }} </div>
|
||||
<div>Identity server: {{ config.identityServer }} </div>
|
||||
<div>User ID: {{ config.user_id }} </div>
|
||||
<div>Access token: {{ config.access_token }} </div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Commands</h3>
|
||||
<div class="section">
|
||||
The following commands are available in the room chat:
|
||||
<ul>
|
||||
<li>/nick <display_name>: change your display name</li>
|
||||
<li>/me <action>: send the action you are doing. /me will be replaced by your display name</li>
|
||||
<li>/join <room_alias>: join a room</li>
|
||||
<li>/kick <user_id> [<reason>]: kick the user</li>
|
||||
<li>/ban <user_id> [<reason>]: ban the user</li>
|
||||
<li>/unban <user_id>: unban the user</li>
|
||||
<li>/op <user_id> <power_level>: set user power level</li>
|
||||
<li>/deop <user_id>: reset user power level to the room default value</li>
|
||||
</ul>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,51 +0,0 @@
|
||||
Testing is done using Karma.
|
||||
|
||||
|
||||
UNIT TESTING
|
||||
============
|
||||
|
||||
Requires the following:
|
||||
- npm/nodejs
|
||||
- phantomjs
|
||||
|
||||
Requires the following node packages:
|
||||
- npm install jasmine
|
||||
- npm install karma
|
||||
- npm install karma-jasmine
|
||||
- npm install karma-phantomjs-launcher
|
||||
- npm install karma-junit-reporter
|
||||
|
||||
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):
|
||||
|
||||
webdriver-manager update
|
||||
webdriver-manager start
|
||||
|
||||
Create a file "environment-protractor.js" in this directory and type:
|
||||
module.exports = {
|
||||
seleniumAddress: 'http://localhost:4444/wd/hub',
|
||||
baseUrl: "http://localhost:8008",
|
||||
username: "YOUR_TEST_USERNAME",
|
||||
password: "YOUR_TEST_PASSWORD"
|
||||
}
|
||||
|
||||
Running e2e tests:
|
||||
protractor protractor.conf.js
|
||||
|
||||
NOTE: This will create a public room on the target home server.
|
||||
|
||||
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
var env = require("../environment-protractor.js");
|
||||
|
||||
describe("home page", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
ptor = protractor.getInstance();
|
||||
// FIXME we use longpoll on the event stream, and I can't get $interval
|
||||
// playing nicely with it. Patches welcome to fix this.
|
||||
ptor.ignoreSynchronization = true;
|
||||
});
|
||||
|
||||
it("should have a title", function() {
|
||||
browser.get(env.baseUrl);
|
||||
expect(browser.getTitle()).toEqual("[matrix]");
|
||||
});
|
||||
});
|
@ -1,107 +0,0 @@
|
||||
// Karma configuration
|
||||
// Generated on Thu Sep 18 2014 14:25:57 GMT+0100 (BST)
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: '',
|
||||
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['jasmine'],
|
||||
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
// XXX: Order is important, and doing /js/angular* makes the tests not run :/
|
||||
files: [
|
||||
'../js/jquery*',
|
||||
'../js/angular.js',
|
||||
'../js/angular-mocks.js',
|
||||
'../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',
|
||||
'../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: [
|
||||
],
|
||||
|
||||
|
||||
// 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', 'junit', 'coverage'],
|
||||
junitReporter: {
|
||||
outputFile: 'test-results.xml',
|
||||
suite: ''
|
||||
},
|
||||
|
||||
coverageReporter: {
|
||||
type: 'cobertura',
|
||||
dir: 'coverage/',
|
||||
file: 'coverage.xml'
|
||||
},
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_DEBUG,
|
||||
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: true,
|
||||
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ['PhantomJS'],
|
||||
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true
|
||||
});
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
var env = require("./environment-protractor.js");
|
||||
exports.config = {
|
||||
seleniumAddress: env.seleniumAddress,
|
||||
specs: ['e2e/*.spec.js'],
|
||||
onPrepare: function() {
|
||||
browser.driver.get(env.baseUrl);
|
||||
browser.driver.findElement(by.id("user_id")).sendKeys(env.username);
|
||||
browser.driver.findElement(by.id("password")).sendKeys(env.password);
|
||||
browser.driver.findElement(by.id("login")).click();
|
||||
|
||||
// wait till the login is done, detect via url change
|
||||
browser.driver.wait(function() {
|
||||
return browser.driver.getCurrentUrl().then(function(url) {
|
||||
return !(/login/.test(url))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
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);
|
||||
}));
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
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;
|
||||
}));
|
||||
|
||||
|
||||
});
|
@ -1,80 +0,0 @@
|
||||
describe('EventStreamService', function() {
|
||||
var q, scope;
|
||||
|
||||
var testInitialSync, testEventStream;
|
||||
|
||||
var matrixService = {
|
||||
initialSync: function(limit, feedback) {
|
||||
var defer = q.defer();
|
||||
defer.resolve(testInitialSync);
|
||||
return defer.promise;
|
||||
},
|
||||
getEventStream: function(from, svrTimeout, cliTimeout) {
|
||||
var defer = q.defer();
|
||||
defer.resolve(testEventStream);
|
||||
return defer.promise;
|
||||
}
|
||||
};
|
||||
|
||||
var eventHandlerService = {
|
||||
handleInitialSyncDone: function(response) {
|
||||
|
||||
},
|
||||
|
||||
handleEvents: function(chunk, isLive) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// setup the dependencies
|
||||
beforeEach(function() {
|
||||
|
||||
// reset test data
|
||||
testInitialSync = {
|
||||
data: {
|
||||
end: "foo",
|
||||
presence: [],
|
||||
rooms: []
|
||||
}
|
||||
};
|
||||
testEventStream = {
|
||||
data: {
|
||||
start: "foostart",
|
||||
end: "fooend",
|
||||
chunk: []
|
||||
}
|
||||
};
|
||||
|
||||
// dependencies
|
||||
module(function ($provide) {
|
||||
$provide.value('matrixService', matrixService);
|
||||
$provide.value('eventHandlerService', eventHandlerService);
|
||||
});
|
||||
|
||||
// tested service
|
||||
module('eventStreamService');
|
||||
});
|
||||
|
||||
beforeEach(inject(function($q, $rootScope) {
|
||||
q = $q;
|
||||
scope = $rootScope;
|
||||
}));
|
||||
|
||||
it('should start with /initialSync then go onto /events', inject(
|
||||
function(eventStreamService) {
|
||||
spyOn(eventHandlerService, "handleInitialSyncDone");
|
||||
spyOn(eventHandlerService, "handleEvents");
|
||||
eventStreamService.resume();
|
||||
scope.$apply(); // initialSync request
|
||||
expect(eventHandlerService.handleInitialSyncDone).toHaveBeenCalledWith(testInitialSync);
|
||||
expect(eventHandlerService.handleEvents).toHaveBeenCalledWith(testEventStream.data.chunk, true);
|
||||
}));
|
||||
|
||||
it('should use the end token in /initialSync for the next /events request', inject(
|
||||
function(eventStreamService) {
|
||||
spyOn(matrixService, "getEventStream").and.callThrough();
|
||||
eventStreamService.resume();
|
||||
scope.$apply(); // initialSync request
|
||||
expect(matrixService.getEventStream).toHaveBeenCalledWith("foo", eventStreamService.SERVER_TIMEOUT, eventStreamService.CLIENT_TIMEOUT);
|
||||
}));
|
||||
});
|
@ -1,822 +0,0 @@
|
||||
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");
|
||||
});
|
||||
|
||||
it("should represent -ve numbers as '0s'", function() {
|
||||
var output = durationFilter(-2000);
|
||||
expect(output).toEqual("0s");
|
||||
});
|
||||
});
|
||||
|
||||
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": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 50
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(output).toEqual([{
|
||||
id: "@a:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 50
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
it("should sort by taking last_active_ago into account", function() {
|
||||
var output = orderMembersList({
|
||||
"@a:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
"@b:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 50
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
"@c:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 99999
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(output).toEqual([
|
||||
{
|
||||
id: "@b:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 50
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@a:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@c:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 99999
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should sort by taking last_updated into account", function() {
|
||||
var output = orderMembersList({
|
||||
"@a:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
"@b:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266900000
|
||||
}
|
||||
},
|
||||
"@c:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943000
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(output).toEqual([
|
||||
{
|
||||
id: "@a:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@c:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943000
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@b:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
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": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943000
|
||||
}
|
||||
},
|
||||
"@b:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 100000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943900
|
||||
}
|
||||
},
|
||||
"@c:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(output).toEqual([
|
||||
{
|
||||
id: "@c:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@a:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943000
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@b:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
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": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
"@b:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 100000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
"@c:example.com": {
|
||||
user: {
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(output).toEqual([
|
||||
{
|
||||
id: "@a:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 1000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@b:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
last_active_ago: 100000
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@c:example.com",
|
||||
user: {
|
||||
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": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
presence: "unavailable",
|
||||
last_active_ago: undefined
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
"@b:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
presence: "online",
|
||||
last_active_ago: undefined
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964,
|
||||
}
|
||||
},
|
||||
"@c:example.com": {
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
presence: "offline",
|
||||
last_active_ago: undefined
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(output).toEqual([
|
||||
{
|
||||
id: "@b:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
presence: "online",
|
||||
last_active_ago: undefined
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@a:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
presence: "unavailable",
|
||||
last_active_ago: undefined
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "@c:example.com",
|
||||
user: {
|
||||
event: {
|
||||
content: {
|
||||
presence: "offline",
|
||||
last_active_ago: undefined
|
||||
}
|
||||
},
|
||||
last_updated: 1415266943964
|
||||
}
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
@ -1,504 +0,0 @@
|
||||
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();
|
||||
}));
|
||||
});
|
@ -1,230 +0,0 @@
|
||||
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);
|
||||
}));
|
||||
|
||||
it('should be able to get a users power level', inject(
|
||||
function(modelService) {
|
||||
var roomId = "!foo:matrix.org";
|
||||
|
||||
var room = modelService.getRoom(roomId);
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: { membership: "join" },
|
||||
user_id: "@adam:matrix.org",
|
||||
state_key: "@adam:matrix.org",
|
||||
type: "m.room.member"
|
||||
});
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: { membership: "join" },
|
||||
user_id: "@beth:matrix.org",
|
||||
state_key: "@beth:matrix.org",
|
||||
type: "m.room.member"
|
||||
});
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: {
|
||||
"@adam:matrix.org": 90,
|
||||
"default": 50
|
||||
},
|
||||
user_id: "@adam:matrix.org",
|
||||
type: "m.room.power_levels"
|
||||
});
|
||||
|
||||
var num = modelService.getUserPowerLevel(roomId, "@beth:matrix.org");
|
||||
expect(num).toEqual(50);
|
||||
|
||||
num = modelService.getUserPowerLevel(roomId, "@adam:matrix.org");
|
||||
expect(num).toEqual(90);
|
||||
|
||||
num = modelService.getUserPowerLevel(roomId, "@unknown:matrix.org");
|
||||
expect(num).toEqual(50);
|
||||
}));
|
||||
|
||||
it('should be able to get a user', inject(
|
||||
function(modelService) {
|
||||
var roomId = "!wefiohwefuiow:matrix.org";
|
||||
var userId = "@bob:matrix.org";
|
||||
|
||||
var presenceEvent = {
|
||||
content: {
|
||||
user_id: userId,
|
||||
displayname: "Bob",
|
||||
last_active_ago: 1415981891580
|
||||
},
|
||||
type: "m.presence",
|
||||
event_id: "weofhwe@matrix.org"
|
||||
};
|
||||
|
||||
modelService.setUser(presenceEvent);
|
||||
var user = modelService.getUser(userId);
|
||||
expect(user.event).toEqual(presenceEvent);
|
||||
}));
|
||||
|
||||
it('should be able to create and get alias mappings.', inject(
|
||||
function(modelService) {
|
||||
var roomId = "!wefiohwefuiow:matrix.org";
|
||||
var alias = "#foobar:matrix.org";
|
||||
|
||||
modelService.createRoomIdToAliasMapping(roomId, alias);
|
||||
|
||||
expect(modelService.getRoomIdToAliasMapping(roomId)).toEqual(alias);
|
||||
expect(modelService.getAliasToRoomIdMapping(alias)).toEqual(roomId);
|
||||
|
||||
}));
|
||||
|
||||
it('should clobber alias mappings.', inject(
|
||||
function(modelService) {
|
||||
var roomId = "!wefiohwefuiow:matrix.org";
|
||||
var alias = "#foobar:matrix.org";
|
||||
var newAlias = "#foobarNEW:matrix.org";
|
||||
|
||||
modelService.createRoomIdToAliasMapping(roomId, alias);
|
||||
|
||||
expect(modelService.getRoomIdToAliasMapping(roomId)).toEqual(alias);
|
||||
expect(modelService.getAliasToRoomIdMapping(alias)).toEqual(roomId);
|
||||
|
||||
modelService.createRoomIdToAliasMapping(roomId, newAlias);
|
||||
|
||||
expect(modelService.getRoomIdToAliasMapping(roomId)).toEqual(newAlias);
|
||||
expect(modelService.getAliasToRoomIdMapping(newAlias)).toEqual(roomId);
|
||||
|
||||
}));
|
||||
|
||||
it('should update RoomMember when User is updated to point to the latest info.', inject(
|
||||
function(modelService) {
|
||||
var roomId = "!wefiohwefuiow:matrix.org";
|
||||
var userId = "@bob:matrix.org";
|
||||
|
||||
var presenceEvent = {
|
||||
content: {
|
||||
user_id: userId,
|
||||
displayname: "Bob",
|
||||
last_active_ago: 1415
|
||||
},
|
||||
type: "m.presence",
|
||||
event_id: "weofhwe@matrix.org"
|
||||
};
|
||||
|
||||
var newPresenceEvent = {
|
||||
content: {
|
||||
user_id: userId,
|
||||
displayname: "The only and only Bob",
|
||||
last_active_ago: 1900
|
||||
},
|
||||
type: "m.presence",
|
||||
event_id: "weofhtweterte@matrix.org"
|
||||
};
|
||||
|
||||
modelService.setUser(presenceEvent);
|
||||
|
||||
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 roomMember = modelService.getMember(roomId, userId);
|
||||
expect(roomMember.user.event).toEqual(presenceEvent);
|
||||
expect(roomMember.user.event.content.displayname).toEqual("Bob");
|
||||
|
||||
modelService.setUser(newPresenceEvent);
|
||||
|
||||
expect(roomMember.user.event.content.displayname).toEqual("The only and only Bob");
|
||||
|
||||
}));
|
||||
|
||||
it('should normalise power levels between 0-100.', inject(
|
||||
function(modelService) {
|
||||
var roomId = "!foo:matrix.org";
|
||||
|
||||
var room = modelService.getRoom(roomId);
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: { membership: "join" },
|
||||
user_id: "@adam:matrix.org",
|
||||
state_key: "@adam:matrix.org",
|
||||
type: "m.room.member"
|
||||
});
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: { membership: "join" },
|
||||
user_id: "@beth:matrix.org",
|
||||
state_key: "@beth:matrix.org",
|
||||
type: "m.room.member"
|
||||
});
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: {
|
||||
"@adam:matrix.org": 1000,
|
||||
"default": 500
|
||||
},
|
||||
user_id: "@adam:matrix.org",
|
||||
type: "m.room.power_levels"
|
||||
});
|
||||
|
||||
var roomMember = modelService.getMember(roomId, "@beth:matrix.org");
|
||||
expect(roomMember.power_level).toEqual(500);
|
||||
expect(roomMember.power_level_norm).toEqual(50);
|
||||
|
||||
|
||||
}));
|
||||
|
||||
it('should be able to get the number of joined users in a room', inject(
|
||||
function(modelService) {
|
||||
var roomId = "!foo:matrix.org";
|
||||
// set mocked data
|
||||
var room = modelService.getRoom(roomId);
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: { membership: "join" },
|
||||
user_id: "@adam:matrix.org",
|
||||
state_key: "@adam:matrix.org",
|
||||
type: "m.room.member"
|
||||
});
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: { membership: "invite" },
|
||||
user_id: "@adam:matrix.org",
|
||||
state_key: "@beth:matrix.org",
|
||||
type: "m.room.member"
|
||||
});
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: { membership: "join" },
|
||||
user_id: "@charlie:matrix.org",
|
||||
state_key: "@charlie:matrix.org",
|
||||
type: "m.room.member"
|
||||
});
|
||||
room.current_room_state.storeStateEvent({
|
||||
content: { membership: "leave" },
|
||||
user_id: "@danice:matrix.org",
|
||||
state_key: "@danice:matrix.org",
|
||||
type: "m.room.member"
|
||||
});
|
||||
|
||||
var num = modelService.getUserCountInRoom(roomId);
|
||||
expect(num).toEqual(2);
|
||||
}));
|
||||
});
|
@ -1,78 +0,0 @@
|
||||
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);
|
||||
}));
|
||||
});
|
@ -1,153 +0,0 @@
|
||||
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({});
|
||||
}));
|
||||
});
|
@ -1,84 +0,0 @@
|
||||
describe("RegisterController ", function() {
|
||||
var rootScope, scope, ctrl, $q, $timeout;
|
||||
var userId = "@foo:bar";
|
||||
var displayName = "Foo";
|
||||
var avatarUrl = "avatar.url";
|
||||
|
||||
window.webClientConfig = {
|
||||
useCaptcha: 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);
|
||||
});
|
||||
});
|
@ -1,57 +0,0 @@
|
||||
describe("UserCtrl", function() {
|
||||
var scope, ctrl, matrixService, routeParams, $q, $timeout;
|
||||
var userId = "@foo:bar";
|
||||
var displayName = "Foo";
|
||||
var avatarUrl = "avatar.url";
|
||||
|
||||
beforeEach(module('matrixWebClient'));
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
inject(function($rootScope, $injector, $controller, _$q_, _$timeout_) {
|
||||
$q = _$q_;
|
||||
$timeout = _$timeout_;
|
||||
|
||||
matrixService = {
|
||||
config: function() {
|
||||
return {
|
||||
user_id: userId
|
||||
};
|
||||
},
|
||||
|
||||
getDisplayName: function(uid) {
|
||||
var d = $q.defer();
|
||||
d.resolve({
|
||||
data: {
|
||||
displayname: displayName
|
||||
}
|
||||
});
|
||||
return d.promise;
|
||||
},
|
||||
|
||||
getProfilePictureUrl: function(uid) {
|
||||
var d = $q.defer();
|
||||
d.resolve({
|
||||
data: {
|
||||
avatar_url: avatarUrl
|
||||
}
|
||||
});
|
||||
return d.promise;
|
||||
}
|
||||
};
|
||||
scope = $rootScope.$new();
|
||||
routeParams = {
|
||||
user_matrix_id: userId
|
||||
};
|
||||
ctrl = $controller('UserController', {
|
||||
'$scope': scope,
|
||||
'$routeParams': routeParams,
|
||||
'matrixService': matrixService
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display your user id', function() {
|
||||
expect(scope.user_id).toEqual(userId);
|
||||
});
|
||||
});
|
@ -1,67 +0,0 @@
|
||||
/*
|
||||
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('UserController', ['matrixService'])
|
||||
.controller('UserController', ['$scope', '$routeParams', 'matrixService',
|
||||
function($scope, $routeParams, matrixService) {
|
||||
$scope.user = {
|
||||
id: $routeParams.user_matrix_id,
|
||||
displayname: "",
|
||||
avatar_url: undefined
|
||||
};
|
||||
|
||||
$scope.user_id = matrixService.config().user_id;
|
||||
|
||||
matrixService.getDisplayName($scope.user.id).then(
|
||||
function(response) {
|
||||
$scope.user.displayname = response.data.displayname;
|
||||
}
|
||||
);
|
||||
|
||||
matrixService.getProfilePictureUrl($scope.user.id).then(
|
||||
function(response) {
|
||||
$scope.user.avatar_url = response.data.avatar_url;
|
||||
}
|
||||
);
|
||||
|
||||
// FIXME: factor this out between user-controller and home-controller etc.
|
||||
$scope.messageUser = function() {
|
||||
|
||||
// FIXME: create a new room every time, for now
|
||||
|
||||
matrixService.create(null, 'private').then(
|
||||
function(response) {
|
||||
// This room has been created. Refresh the rooms list
|
||||
var room_id = response.data.room_id;
|
||||
console.log("Created room with id: "+ room_id);
|
||||
|
||||
matrixService.invite(room_id, $scope.user.id).then(
|
||||
function() {
|
||||
$scope.feedback = "Invite sent successfully";
|
||||
$scope.$parent.goToPage("/room/" + room_id);
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failure: " + JSON.stringify(reason);
|
||||
});
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + JSON.stringify(error.data);
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
@ -1,25 +0,0 @@
|
||||
<div ng-controller="UserController" class="user">
|
||||
|
||||
<div id="wrapper">
|
||||
|
||||
<div id="genericHeading">
|
||||
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
|
||||
</div>
|
||||
|
||||
<h1>{{ user.displayname || user.id }}</h1>
|
||||
|
||||
<div>
|
||||
<div class="profile-avatar">
|
||||
<img ng-src="{{ user.avatar_url || 'img/default-profile.png' }}"/>
|
||||
</div>
|
||||
<div id="user-ids">
|
||||
<div>{{ user.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button ng-hide="user.id == user_id" ng-click="messageUser()" style="font-size: 14pt; margin-top: 40px; margin-bottom: 40px">Start chat</button>
|
||||
<br/>
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|