Reference Matrix Home Server

This commit is contained in:
matrix.org 2014-08-12 15:10:52 +01:00
commit 4f475c7697
217 changed files with 48447 additions and 0 deletions

12
webclient/README Normal file
View 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/

View 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
View 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
View 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);
}
};
}]);

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
webclient/img/gradient.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

41
webclient/index.html Normal file
View 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

File diff suppressed because it is too large Load diff

927
webclient/js/angular-route.js vendored Normal file
View 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

File diff suppressed because it is too large Load diff

View 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);
}
}
);
};
}]);

View 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>

View 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
View 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>

View 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();
}]);

View 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>