mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-08-10 23:10:10 -04:00
Reference Matrix Home Server
This commit is contained in:
commit
4f475c7697
217 changed files with 48447 additions and 0 deletions
12
webclient/README
Normal file
12
webclient/README
Normal file
|
@ -0,0 +1,12 @@
|
|||
Basic Usage
|
||||
-----------
|
||||
|
||||
The Synapse web client needs to be hosted by a basic HTTP server.
|
||||
|
||||
You can use the Python simple HTTP server::
|
||||
|
||||
$ python -m SimpleHTTPServer
|
||||
|
||||
Then, open this URL in a WEB browser::
|
||||
|
||||
http://127.0.0.1:8000/
|
46
webclient/app-controller.js
Normal file
46
webclient/app-controller.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Main controller
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('MatrixWebClientController', ['matrixService'])
|
||||
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService',
|
||||
function($scope, $location, $rootScope, matrixService) {
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
|
||||
// Manage the display of the current config
|
||||
$scope.config;
|
||||
|
||||
// Toggles the config display
|
||||
$scope.showConfig = function() {
|
||||
if ($scope.config) {
|
||||
$scope.config = undefined;
|
||||
}
|
||||
else {
|
||||
$scope.config = matrixService.config();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Logs the user out
|
||||
$scope.logout = function() {
|
||||
// Clean permanent data
|
||||
matrixService.setConfig({});
|
||||
matrixService.saveConfig();
|
||||
|
||||
// And go to the login page
|
||||
$location.path("login");
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
|
228
webclient/app.css
Normal file
228
webclient/app.css
Normal file
|
@ -0,0 +1,228 @@
|
|||
body {
|
||||
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
||||
font-size: 12pt;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/*** Overall page layout ***/
|
||||
|
||||
.page {
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
margin-bottom: 80px ! important;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.roomName {
|
||||
text-align: right;
|
||||
font-size: 16pt;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.controlPanel {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
background-color: #f8f8f8;
|
||||
border-top: #aaa 1px solid;
|
||||
}
|
||||
|
||||
.controls {
|
||||
max-width: 1280px;
|
||||
padding: 12px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.inputBarTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputBarTable tr td {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.mainInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*** Participant list ***/
|
||||
|
||||
.usersTable {
|
||||
float: right;
|
||||
width: 120px;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 150px;
|
||||
}
|
||||
|
||||
.usersTable td {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.userAvatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.userAvatar .userAvatarImage {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.userAvatar .userAvatarGradient {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.userAvatar .userName {
|
||||
position: absolute;
|
||||
color: #fff;
|
||||
margin: 2px;
|
||||
bottom: 0px;
|
||||
font-size: 8pt;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.userPresence {
|
||||
text-align: center;
|
||||
font-size: 8pt;
|
||||
color: #fff;
|
||||
background-color: #aaa;
|
||||
border-bottom: 1px #ddd solid;
|
||||
}
|
||||
|
||||
.online {
|
||||
background-color: #38AF00;
|
||||
}
|
||||
|
||||
.away {
|
||||
background-color: #FFCC00;
|
||||
}
|
||||
|
||||
|
||||
/*** Message table ***/
|
||||
|
||||
.messageTableWrapper {
|
||||
width: auto;
|
||||
margin-right: 140px;
|
||||
}
|
||||
|
||||
.messageTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.messageTable td {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.leftBlock {
|
||||
width: 1px;
|
||||
vertical-align: top;
|
||||
background-color: #fff;
|
||||
color: #888;
|
||||
font-weight: medium;
|
||||
font-size: 8pt;
|
||||
text-align: right;
|
||||
border-top: 1px #ddd solid;
|
||||
}
|
||||
|
||||
.rightBlock {
|
||||
width: 32px;
|
||||
color: #888;
|
||||
line-height: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.sender, .timestamp {
|
||||
padding-right: 1em;
|
||||
padding-left: 1em;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.sender {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
background-color: #eee;
|
||||
border: 1px solid #d8d8d8;
|
||||
height: 32px;
|
||||
display: inline-table;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.emote {
|
||||
background-color: #fff ! important;
|
||||
border: 0px ! important;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 6px;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.mine {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mine .text .bubble {
|
||||
text-align: left ! important;
|
||||
background-color: #d8d8e8 ! important;
|
||||
}
|
||||
|
||||
.mine .emote .bubble {
|
||||
background-color: #fff ! important;
|
||||
}
|
||||
|
||||
/******************************/
|
||||
|
||||
.header {
|
||||
margin-top: 12px ! important;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.text_entry_section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
left: 0;
|
||||
right: 10em;
|
||||
width: 100%;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.member_invited {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.member_joined {
|
||||
|
||||
}
|
||||
|
||||
.member_left {
|
||||
color: gray;
|
||||
}
|
57
webclient/app.js
Normal file
57
webclient/app.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
var matrixWebClient = angular.module('matrixWebClient', [
|
||||
'ngRoute',
|
||||
'MatrixWebClientController',
|
||||
'LoginController',
|
||||
'RoomController',
|
||||
'RoomsController',
|
||||
'matrixService'
|
||||
]);
|
||||
|
||||
matrixWebClient.config(['$routeProvider',
|
||||
function($routeProvider) {
|
||||
$routeProvider.
|
||||
when('/login', {
|
||||
templateUrl: 'login/login.html',
|
||||
controller: 'LoginController'
|
||||
}).
|
||||
when('/room/:room_id', {
|
||||
templateUrl: 'room/room.html',
|
||||
controller: 'RoomController'
|
||||
}).
|
||||
when('/rooms', {
|
||||
templateUrl: 'rooms/rooms.html',
|
||||
controller: 'RoomsController'
|
||||
}).
|
||||
otherwise({
|
||||
redirectTo: '/rooms'
|
||||
});
|
||||
}]);
|
||||
|
||||
matrixWebClient.run(['$location', 'matrixService' , function($location, matrixService) {
|
||||
// If we have no persistent login information, go to the login page
|
||||
var config = matrixService.config();
|
||||
if (!config || !config.access_token) {
|
||||
$location.path("login");
|
||||
}
|
||||
}]);
|
||||
|
||||
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) {
|
||||
$timeout(function() { element[0].focus() }, 0);
|
||||
}
|
||||
};
|
||||
}]);
|
307
webclient/components/matrix/matrix-service.js
Normal file
307
webclient/components/matrix/matrix-service.js
Normal file
|
@ -0,0 +1,307 @@
|
|||
'use strict';
|
||||
|
||||
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 MAPPING_PREFIX = "alias_for_";
|
||||
|
||||
var doRequest = function(method, path, params, data) {
|
||||
// Inject the access token
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
params.access_token = config.access_token;
|
||||
|
||||
return doBaseRequest(config.homeserver, method, path, params, data, undefined);
|
||||
};
|
||||
|
||||
var doBaseRequest = function(baseUrl, method, path, params, data, headers) {
|
||||
if (path.indexOf(prefixPath) !== 0) {
|
||||
path = prefixPath + path;
|
||||
}
|
||||
// Do not directly return the $http instance but return a promise
|
||||
// with enriched or cleaned information
|
||||
var deferred = $q.defer();
|
||||
$http({
|
||||
method: method,
|
||||
url: baseUrl + path,
|
||||
params: params,
|
||||
data: data,
|
||||
headers: headers
|
||||
})
|
||||
.success(function(data, status, headers, config) {
|
||||
// @TODO: We could detect a bad access token here and make an automatic logout
|
||||
deferred.resolve(data, status, headers, config);
|
||||
})
|
||||
.error(function(data, status, headers, config) {
|
||||
// Enrich the error callback with an human readable error reason
|
||||
var reason = data.error;
|
||||
if (!data.error) {
|
||||
reason = JSON.stringify(data);
|
||||
}
|
||||
deferred.reject(reason, data, status, headers, config);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
/****** Home server API ******/
|
||||
prefix: prefixPath,
|
||||
|
||||
// Register an user
|
||||
register: function(user_name, password) {
|
||||
// The REST path spec
|
||||
var path = "/register";
|
||||
|
||||
return doRequest("POST", path, undefined, {
|
||||
user_id: user_name,
|
||||
password: password
|
||||
});
|
||||
},
|
||||
|
||||
// Create a room
|
||||
create: function(room_id, visibility) {
|
||||
// The REST path spec
|
||||
var path = "/rooms";
|
||||
|
||||
return doRequest("POST", path, undefined, {
|
||||
visibility: visibility,
|
||||
room_alias_name: room_id
|
||||
});
|
||||
},
|
||||
|
||||
// List all rooms joined or been invited to
|
||||
rooms: function(from, to, limit) {
|
||||
// The REST path spec
|
||||
var path = "/im/sync";
|
||||
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
// Joins a room
|
||||
join: function(room_id) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", config.user_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, {
|
||||
membership: "join"
|
||||
});
|
||||
},
|
||||
|
||||
// Invite a user to a room
|
||||
invite: function(room_id, user_id) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", user_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, {
|
||||
membership: "invite"
|
||||
});
|
||||
},
|
||||
|
||||
// Leaves a room
|
||||
leave: function(room_id) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", config.user_id);
|
||||
|
||||
return doRequest("DELETE", path, undefined, undefined);
|
||||
},
|
||||
|
||||
sendMessage: function(room_id, msg_id, content) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/messages/$from/$msg_id";
|
||||
|
||||
if (!msg_id) {
|
||||
msg_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("$from", config.user_id);
|
||||
path = path.replace("$msg_id", msg_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, 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 emote message
|
||||
sendEmoteMessage: function(room_id, body, msg_id) {
|
||||
var content = {
|
||||
msgtype: "m.emote",
|
||||
body: body
|
||||
};
|
||||
|
||||
return this.sendMessage(room_id, msg_id, 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/list";
|
||||
path = path.replace("$room_id", room_id);
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
// get a list of public rooms on your home server
|
||||
publicRooms: function() {
|
||||
var path = "/public/rooms"
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
// 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", config.user_id);
|
||||
return doRequest("PUT", path, undefined, data);
|
||||
},
|
||||
|
||||
getProfileInfo: function(userId, info_segment) {
|
||||
var path = "/profile/$user_id/" + info_segment;
|
||||
path = path.replace("$user_id", userId);
|
||||
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) {
|
||||
var path = "/matrix/identity/api/v1/validate/email/requestToken"
|
||||
var data = "clientSecret=abc123&email=" + encodeURIComponent(email);
|
||||
var headers = {};
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||
},
|
||||
|
||||
authEmail: function(userId, tokenId, code) {
|
||||
var path = "/matrix/identity/api/v1/validate/email/submitToken";
|
||||
var data = "token="+code+"&mxId="+encodeURIComponent(userId)+"&tokenId="+tokenId;
|
||||
var headers = {};
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||
},
|
||||
|
||||
/****** 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;
|
||||
},
|
||||
|
||||
// Commits config into permanent storage
|
||||
saveConfig: function() {
|
||||
config.version = configVersion;
|
||||
localStorage.setItem("config", JSON.stringify(config));
|
||||
},
|
||||
|
||||
createRoomIdToAliasMapping: function(roomId, alias) {
|
||||
localStorage.setItem(MAPPING_PREFIX+roomId, alias);
|
||||
},
|
||||
|
||||
getRoomIdToAliasMapping: function(roomId) {
|
||||
return localStorage.getItem(MAPPING_PREFIX+roomId);
|
||||
}
|
||||
|
||||
};
|
||||
}]);
|
BIN
webclient/favicon.ico
Normal file
BIN
webclient/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 198 B |
BIN
webclient/img/default-profile.jpg
Normal file
BIN
webclient/img/default-profile.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
webclient/img/default-profile.png
Normal file
BIN
webclient/img/default-profile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
webclient/img/gradient.png
Normal file
BIN
webclient/img/gradient.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 194 B |
41
webclient/index.html
Normal file
41
webclient/index.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
<!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="icon" href="favicon.ico">
|
||||
|
||||
<script src="js/angular.js"></script>
|
||||
<script src="js/angular-route.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="app-controller.js"></script>
|
||||
<script src="login/login-controller.js"></script>
|
||||
<script src="room/room-controller.js"></script>
|
||||
<script src="rooms/rooms-controller.js"></script>
|
||||
<script src="components/matrix/matrix-service.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<!-- Do not show buttons on the login page -->
|
||||
<div class="header-buttons" ng-hide="'/login' == location ">
|
||||
<button ng-click="showConfig()">Config</button>
|
||||
<button ng-click="logout()">Log out</button>
|
||||
</div>
|
||||
|
||||
<h1>[matrix]</h1>
|
||||
</header>
|
||||
|
||||
<div class="config" ng-hide="!config">
|
||||
<div>Home server: {{ config.homeserver }} </div>
|
||||
<div>User ID: {{ config.user_id }} </div>
|
||||
<div>Access token: {{ config.access_token }} </div>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
</html>
|
1689
webclient/js/angular-animate.js
vendored
Normal file
1689
webclient/js/angular-animate.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
927
webclient/js/angular-route.js
vendored
Normal file
927
webclient/js/angular-route.js
vendored
Normal file
|
@ -0,0 +1,927 @@
|
|||
/**
|
||||
* @license AngularJS v1.2.20
|
||||
* (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);
|
||||
|
||||
/**
|
||||
* @ngdoc provider
|
||||
* @name $routeProvider
|
||||
* @kind function
|
||||
*
|
||||
* @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} params Mapping information to be assigned to `$route.current`.
|
||||
* @returns {Object} self
|
||||
*/
|
||||
this.otherwise = function(params) {
|
||||
this.when(null, params);
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
this.$get = ['$rootScope',
|
||||
'$location',
|
||||
'$routeParams',
|
||||
'$q',
|
||||
'$injector',
|
||||
'$http',
|
||||
'$templateCache',
|
||||
'$sce',
|
||||
function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $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);
|
||||
}
|
||||
};
|
||||
|
||||
$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 = 'string' == typeof m[i]
|
||||
? decodeURIComponent(m[i])
|
||||
: 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);
|
||||
});
|
||||
|
||||
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 = $http.get(templateUrl, {cache: $templateCache}).
|
||||
then(function(response) { return response.data; });
|
||||
}
|
||||
}
|
||||
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>$route.current.scope.name = {{main.$route.current.scope.name}}</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, 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, 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);
|
21822
webclient/js/angular.js
vendored
Normal file
21822
webclient/js/angular.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
81
webclient/login/login-controller.js
Normal file
81
webclient/login/login-controller.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
angular.module('LoginController', ['matrixService'])
|
||||
.controller('LoginController', ['$scope', '$location', 'matrixService',
|
||||
function($scope, $location, matrixService) {
|
||||
'use strict';
|
||||
|
||||
$scope.account = {
|
||||
homeserver: "http://localhost:8080",
|
||||
desired_user_name: "",
|
||||
user_id: "",
|
||||
password: "",
|
||||
identityServer: "http://localhost:8090",
|
||||
pwd1: "",
|
||||
pwd2: ""
|
||||
};
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
matrixService.register($scope.account.desired_user_name, $scope.account.pwd1).then(
|
||||
function(data) {
|
||||
$scope.feedback = "Success";
|
||||
|
||||
// Update the current config
|
||||
var config = matrixService.config();
|
||||
angular.extend(config, {
|
||||
access_token: data.access_token,
|
||||
user_id: data.user_id
|
||||
});
|
||||
matrixService.setConfig(config);
|
||||
|
||||
// And permanently save it
|
||||
matrixService.saveConfig();
|
||||
|
||||
// Go to the user's rooms list page
|
||||
$location.path("rooms");
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failure: " + reason;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.login = function() {
|
||||
matrixService.setConfig({
|
||||
homeserver: $scope.account.homeserver,
|
||||
user_id: $scope.account.user_id
|
||||
});
|
||||
// try to login
|
||||
matrixService.login($scope.account.user_id, $scope.account.password).then(
|
||||
function(response) {
|
||||
if ("access_token" in response) {
|
||||
$scope.feedback = "Login successful.";
|
||||
matrixService.setConfig({
|
||||
homeserver: $scope.account.homeserver,
|
||||
user_id: $scope.account.user_id,
|
||||
access_token: response.access_token
|
||||
});
|
||||
matrixService.saveConfig();
|
||||
$location.path("rooms");
|
||||
}
|
||||
else {
|
||||
$scope.feedback = "Failed to login: " + JSON.stringify(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}]);
|
||||
|
51
webclient/login/login.html
Normal file
51
webclient/login/login.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
<div ng-controller="LoginController" class="login">
|
||||
<div class="page">
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
<h3>Register for an account:</h3>
|
||||
<form novalidate>
|
||||
<input id="desired_user_name" size="70" type="text" auto-focus ng-model="account.desired_user_name" placeholder="User name (ex:bob)"/>
|
||||
<br/>
|
||||
<input id="pwd1" size="70" type="password" auto-focus ng-model="account.pwd1" placeholder="Type a password"/>
|
||||
<br/>
|
||||
<input id="pwd2" size="70" type="password" auto-focus ng-model="account.pwd2" placeholder="Re-type your password"/>
|
||||
<br/>
|
||||
<!-- New user registration -->
|
||||
<div>
|
||||
<br/>
|
||||
<button ng-click="register()" ng-disabled="!account.desired_user_name || !account.homeserver || !account.identityServer || !account.pwd1 || !account.pwd2">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h3>Got an account?</h3>
|
||||
<form novalidate>
|
||||
<!-- Login with an registered user -->
|
||||
<div>
|
||||
<input id="user_id" size="70" type="text" auto-focus ng-model="account.user_id" placeholder="User ID (ex:@bob:localhost)"/>
|
||||
<br />
|
||||
<input id="password" size="70" type="password" ng-model="account.password" placeholder="Password"/><br />
|
||||
<br/>
|
||||
<button ng-click="login()" ng-disabled="!account.user_id || !account.password || !account.homeserver || !account.identityServer">Login</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<h3>Servers</h3>
|
||||
<form novalidate>
|
||||
<div>
|
||||
Home Server:
|
||||
<input id="homeserver" size="57" type="text" ng-model="account.homeserver" placeholder="Home server URL (ex: http://localhost:8080)"/>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
Identity Server:
|
||||
<input id="identityServer" size="56" type="text" ng-model="account.identityServer" placeholder="Identity server URL (ex: http://localhost:8090)"/>
|
||||
</div>
|
||||
<br />
|
||||
</form>
|
||||
<br/>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
214
webclient/room/room-controller.js
Normal file
214
webclient/room/room-controller.js
Normal file
|
@ -0,0 +1,214 @@
|
|||
angular.module('RoomController', [])
|
||||
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService',
|
||||
function($scope, $http, $timeout, $routeParams, $location, matrixService) {
|
||||
'use strict';
|
||||
$scope.room_id = $routeParams.room_id;
|
||||
$scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
|
||||
$scope.state = {
|
||||
user_id: matrixService.config().user_id,
|
||||
events_from: "START"
|
||||
};
|
||||
$scope.messages = [];
|
||||
$scope.members = {};
|
||||
$scope.stopPoll = false;
|
||||
|
||||
$scope.userIDToInvite = "";
|
||||
|
||||
var shortPoll = function() {
|
||||
$http.get(matrixService.config().homeserver + matrixService.prefix + "/events", {
|
||||
"params": {
|
||||
"access_token": matrixService.config().access_token,
|
||||
"from": $scope.state.events_from,
|
||||
"timeout": 5000
|
||||
}})
|
||||
.then(function(response) {
|
||||
console.log("Got response from "+$scope.state.events_from+" to "+response.data.end);
|
||||
$scope.state.events_from = response.data.end;
|
||||
|
||||
for (var i = 0; i < response.data.chunk.length; i++) {
|
||||
var chunk = response.data.chunk[i];
|
||||
if (chunk.room_id == $scope.room_id && chunk.type == "m.room.message") {
|
||||
if ("membership_target" in chunk.content) {
|
||||
chunk.user_id = chunk.content.membership_target;
|
||||
}
|
||||
$scope.messages.push(chunk);
|
||||
$timeout(function() {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
},0);
|
||||
}
|
||||
else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") {
|
||||
updateMemberList(chunk);
|
||||
}
|
||||
else if (chunk.type === "m.presence") {
|
||||
updatePresence(chunk);
|
||||
}
|
||||
}
|
||||
if ($scope.stopPoll) {
|
||||
console.log("Stopping polling.");
|
||||
}
|
||||
else {
|
||||
$timeout(shortPoll, 0);
|
||||
}
|
||||
}, function(response) {
|
||||
$scope.feedback = "Can't stream: " + JSON.stringify(response);
|
||||
if ($scope.stopPoll) {
|
||||
console.log("Stopping polling.");
|
||||
}
|
||||
else {
|
||||
$timeout(shortPoll, 2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var updateMemberList = function(chunk) {
|
||||
var isNewMember = !(chunk.target_user_id in $scope.members);
|
||||
if (isNewMember) {
|
||||
$scope.members[chunk.target_user_id] = chunk;
|
||||
// get their display name and profile picture and set it to their
|
||||
// member entry in $scope.members. We HAVE to use $timeout with 0 delay
|
||||
// to make this function run AFTER the current digest cycle, else the
|
||||
// response may update a STALE VERSION of the member list (manifesting
|
||||
// as no member names appearing, or appearing sporadically).
|
||||
$scope.$evalAsync(function() {
|
||||
matrixService.getDisplayName(chunk.target_user_id).then(
|
||||
function(response) {
|
||||
var member = $scope.members[chunk.target_user_id];
|
||||
if (member !== undefined) {
|
||||
console.log("Updated displayname "+chunk.target_user_id+" to " + response.displayname);
|
||||
member.displayname = response.displayname;
|
||||
}
|
||||
}
|
||||
);
|
||||
matrixService.getProfilePictureUrl(chunk.target_user_id).then(
|
||||
function(response) {
|
||||
var member = $scope.members[chunk.target_user_id];
|
||||
if (member !== undefined) {
|
||||
console.log("Updated image for "+chunk.target_user_id+" to " + response.avatar_url);
|
||||
member.avatar_url = response.avatar_url;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// selectively update membership else it will nuke the picture and displayname too :/
|
||||
var member = $scope.members[chunk.target_user_id];
|
||||
member.content.membership = chunk.content.membership;
|
||||
}
|
||||
}
|
||||
|
||||
var updatePresence = function(chunk) {
|
||||
if (!(chunk.content.user_id in $scope.members)) {
|
||||
console.log("updatePresence: Unknown member for chunk " + JSON.stringify(chunk));
|
||||
return;
|
||||
}
|
||||
var member = $scope.members[chunk.content.user_id];
|
||||
|
||||
if ("state" in chunk.content) {
|
||||
var ONLINE = 2;
|
||||
var AWAY = 1;
|
||||
var OFFLINE = 0;
|
||||
if (chunk.content.state === ONLINE) {
|
||||
member.presenceState = "online";
|
||||
}
|
||||
else if (chunk.content.state === OFFLINE) {
|
||||
member.presenceState = "offline";
|
||||
}
|
||||
else if (chunk.content.state === AWAY) {
|
||||
member.presenceState = "away";
|
||||
}
|
||||
}
|
||||
|
||||
// this may also contain a new display name or avatar url, so check.
|
||||
if ("displayname" in chunk.content) {
|
||||
member.displayname = chunk.content.displayname;
|
||||
}
|
||||
|
||||
if ("avatar_url" in chunk.content) {
|
||||
member.avatar_url = chunk.content.avatar_url;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.send = function() {
|
||||
if ($scope.textInput == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the text message
|
||||
var promise;
|
||||
// FIXME: handle other commands too
|
||||
if ($scope.textInput.indexOf("/me") == 0) {
|
||||
promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4));
|
||||
}
|
||||
else {
|
||||
promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput);
|
||||
}
|
||||
|
||||
promise.then(
|
||||
function() {
|
||||
console.log("Sent message");
|
||||
$scope.textInput = "";
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failed to send: " + reason;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onInit = function() {
|
||||
// $timeout(function() { document.getElementById('textInput').focus() }, 0);
|
||||
console.log("onInit");
|
||||
|
||||
// Join the room
|
||||
matrixService.join($scope.room_id).then(
|
||||
function() {
|
||||
console.log("Joined room");
|
||||
// Now start reading from the stream
|
||||
$timeout(shortPoll, 0);
|
||||
|
||||
// Get the current member list
|
||||
matrixService.getMemberList($scope.room_id).then(
|
||||
function(response) {
|
||||
for (var i = 0; i < response.chunk.length; i++) {
|
||||
var chunk = response.chunk[i];
|
||||
updateMemberList(chunk);
|
||||
}
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failed get member list: " + reason;
|
||||
}
|
||||
);
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Can't join room: " + reason;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.inviteUser = function(user_id) {
|
||||
|
||||
matrixService.invite($scope.room_id, user_id).then(
|
||||
function() {
|
||||
console.log("Invited.");
|
||||
$scope.feedback = "Request for invitation succeeds";
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failure: " + reason;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.leaveRoom = function() {
|
||||
|
||||
matrixService.leave($scope.room_id).then(
|
||||
function(response) {
|
||||
console.log("Left room ");
|
||||
$location.path("rooms");
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failed to leave room: " + reason;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function(e) {
|
||||
console.log("onDestroyed: Stopping poll.");
|
||||
$scope.stopPoll = true;
|
||||
});
|
||||
}]);
|
76
webclient/room/room.html
Normal file
76
webclient/room/room.html
Normal file
|
@ -0,0 +1,76 @@
|
|||
<div ng-controller="RoomController" data-ng-init="onInit()" class="room">
|
||||
|
||||
<div class="page">
|
||||
|
||||
<div class="roomName">
|
||||
{{ room_alias || room_id }}
|
||||
</div>
|
||||
|
||||
<table class="usersTable">
|
||||
<tr ng-repeat="(name, info) in members">
|
||||
<td class="userAvatar">
|
||||
<img class="userAvatarImage" ng-src="{{info.avatar_url || 'img/default-profile.jpg'}}" width="80" height="80"/>
|
||||
<img class="userAvatarGradient" src="img/gradient.png" width="80" height="24"/>
|
||||
<div class="userName">{{ info.displayname || name }}</div>
|
||||
</td>
|
||||
<td class="userPresence" ng-class="info.presenceState === 'online' ? 'online' : (info.presenceState === 'away' ? 'away' : '')" />
|
||||
</table>
|
||||
|
||||
<div class="messageTableWrapper">
|
||||
<table class="messageTable">
|
||||
<tr ng-repeat="msg in messages" ng-class="msg.user_id === state.user_id ? 'mine' : ''">
|
||||
<td class="leftBlock">
|
||||
<div class="sender" ng-hide="messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
|
||||
<div class="timestamp">{{ msg.content.hsob_ts | date:'HH:mm:ss' }}</div>
|
||||
</td>
|
||||
<td class="avatar">
|
||||
<img ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
|
||||
ng-hide="messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
|
||||
</td>
|
||||
<td ng-class="!msg.content.membership_target ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : ''">
|
||||
<div class="bubble">
|
||||
{{ msg.content.msgtype === "m.emote" ? ("* " + (members[msg.user_id].displayname || msg.user_id) + " ") : "" }}
|
||||
{{ msg.content.body }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="rightBlock">
|
||||
<img ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
|
||||
ng-hide="messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="controlPanel">
|
||||
<div class="controls">
|
||||
<table class="inputBarTable">
|
||||
<tr>
|
||||
<td width="1">
|
||||
{{ state.user_id }}
|
||||
</td>
|
||||
<td width="*">
|
||||
<input class="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true"/>
|
||||
</td>
|
||||
<td width="1">
|
||||
<button ng-click="send()">Send</button>
|
||||
</td>
|
||||
<td width="1">
|
||||
{{ feedback }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<span>
|
||||
Invite a user:
|
||||
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
|
||||
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
|
||||
</span>
|
||||
<button ng-click="leaveRoom()">Leave</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
195
webclient/rooms/rooms-controller.js
Normal file
195
webclient/rooms/rooms-controller.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('RoomsController', ['matrixService'])
|
||||
.controller('RoomsController', ['$scope', '$location', 'matrixService',
|
||||
function($scope, $location, matrixService) {
|
||||
|
||||
$scope.rooms = [];
|
||||
$scope.public_rooms = [];
|
||||
$scope.newRoomId = "";
|
||||
$scope.feedback = "";
|
||||
|
||||
$scope.newRoom = {
|
||||
room_id: "",
|
||||
private: false
|
||||
};
|
||||
|
||||
$scope.goToRoom = {
|
||||
room_id: "",
|
||||
};
|
||||
|
||||
$scope.newProfileInfo = {
|
||||
name: matrixService.config().displayName,
|
||||
avatar: matrixService.config().avatarUrl
|
||||
};
|
||||
|
||||
$scope.linkedEmails = {
|
||||
linkNewEmail: "", // the email entry box
|
||||
emailBeingAuthed: undefined, // to populate verification text
|
||||
authTokenId: undefined, // the token id from the IS
|
||||
emailCode: "", // the code entry box
|
||||
linkedEmailList: matrixService.config().emailList // linked email list
|
||||
};
|
||||
|
||||
var assignRoomAliases = function(data) {
|
||||
for (var i=0; i<data.length; i++) {
|
||||
var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id);
|
||||
if (alias) {
|
||||
data[i].room_alias = alias;
|
||||
}
|
||||
else {
|
||||
data[i].room_alias = data[i].room_id;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
$scope.refresh = function() {
|
||||
// List all rooms joined or been invited to
|
||||
$scope.rooms = matrixService.rooms();
|
||||
matrixService.rooms().then(
|
||||
function(data) {
|
||||
data = assignRoomAliases(data);
|
||||
$scope.feedback = "Success";
|
||||
$scope.rooms = data;
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failure: " + reason;
|
||||
});
|
||||
|
||||
matrixService.publicRooms().then(
|
||||
function(data) {
|
||||
$scope.public_rooms = assignRoomAliases(data.chunk);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.createNewRoom = function(room_id, isPrivate) {
|
||||
|
||||
var visibility = "public";
|
||||
if (isPrivate) {
|
||||
visibility = "private";
|
||||
}
|
||||
|
||||
matrixService.create(room_id, visibility).then(
|
||||
function(response) {
|
||||
// This room has been created. Refresh the rooms list
|
||||
console.log("Created room " + response.room_alias + " with id: "+
|
||||
response.room_id);
|
||||
matrixService.createRoomIdToAliasMapping(
|
||||
response.room_id, response.room_alias);
|
||||
$scope.refresh();
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Failure: " + reason;
|
||||
});
|
||||
};
|
||||
|
||||
// Go to a room
|
||||
$scope.goToRoom = function(room_id) {
|
||||
// Simply open the room page on this room id
|
||||
//$location.path("room/" + room_id);
|
||||
matrixService.join(room_id).then(
|
||||
function(response) {
|
||||
if (response.hasOwnProperty("room_id")) {
|
||||
if (response.room_id != room_id) {
|
||||
$location.path("room/" + response.room_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$location.path("room/" + room_id);
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Can't join room: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.setDisplayName = function(newName) {
|
||||
matrixService.setDisplayName(newName).then(
|
||||
function(response) {
|
||||
$scope.feedback = "Updated display name.";
|
||||
var config = matrixService.config();
|
||||
config.displayName = newName;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Can't update display name: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.setAvatar = function(newUrl) {
|
||||
console.log("Updating avatar to "+newUrl);
|
||||
matrixService.setProfilePictureUrl(newUrl).then(
|
||||
function(response) {
|
||||
console.log("Updated avatar");
|
||||
$scope.feedback = "Updated avatar.";
|
||||
var config = matrixService.config();
|
||||
config.avatarUrl = newUrl;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Can't update avatar: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.linkEmail = function(email) {
|
||||
matrixService.linkEmail(email).then(
|
||||
function(response) {
|
||||
if (response.success === true) {
|
||||
$scope.linkedEmails.authTokenId = response.tokenId;
|
||||
$scope.emailFeedback = "You have been sent an email.";
|
||||
$scope.linkedEmails.emailBeingAuthed = email;
|
||||
}
|
||||
else {
|
||||
$scope.emailFeedback = "Failed to send email.";
|
||||
}
|
||||
},
|
||||
function(reason) {
|
||||
$scope.emailFeedback = "Can't send email: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.submitEmailCode = function(code) {
|
||||
var tokenId = $scope.linkedEmails.authTokenId;
|
||||
if (tokenId === undefined) {
|
||||
$scope.emailFeedback = "You have not requested a code with this email.";
|
||||
return;
|
||||
}
|
||||
matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
|
||||
function(response) {
|
||||
if ("success" in response && response.success === false) {
|
||||
$scope.emailFeedback = "Failed to authenticate email.";
|
||||
return;
|
||||
}
|
||||
var config = matrixService.config();
|
||||
var emailList = {};
|
||||
if ("emailList" in config) {
|
||||
emailList = config.emailList;
|
||||
}
|
||||
emailList[response.address] = 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 auth email: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.refresh();
|
||||
}]);
|
80
webclient/rooms/rooms.html
Normal file
80
webclient/rooms/rooms.html
Normal file
|
@ -0,0 +1,80 @@
|
|||
<div ng-controller="RoomsController" class="rooms">
|
||||
|
||||
<div class="page">
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newProfileInfo.name" ng-enter="setDisplayName(newProfileInfo.name)" />
|
||||
<button ng-disabled="!newProfileInfo.name" ng-click="setDisplayName(newProfileInfo.name)">Update Name</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newProfileInfo.avatar" ng-enter="setAvatar(newProfileInfo.avatar)" />
|
||||
<button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar</button>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
Linked emails:
|
||||
<table>
|
||||
<tr ng-repeat="(address, info) in linkedEmails.linkedEmailList">
|
||||
<td>{{address}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>My rooms</h3>
|
||||
|
||||
<div class="rooms" ng-repeat="room in rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_id }}" >{{ room.room_alias }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Public rooms</h3>
|
||||
|
||||
<div class="public_rooms" ng-repeat="room in public_rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_id }}" >{{ room.room_alias }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
|
||||
<input type="checkbox" ng-model="newRoom.private">private
|
||||
<button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="goToRoom.room_id" ng-enter="goToRoom(goToRoom.room_id)" placeholder="(e.g. #foo_channe:example.org)"/>
|
||||
<button ng-disabled="!goToRoom.room_id" ng-click="goToRoom(goToRoom.room_id)">Go to room</button>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue