diff options
Diffstat (limited to 'webclient')
29 files changed, 1019 insertions, 497 deletions
diff --git a/webclient/app-controller.js b/webclient/app-controller.js index 80474bb8df..42c45f7c31 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -21,8 +21,8 @@ limitations under the License. 'use strict'; angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService']) -.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', - function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) { +.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService', + function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService, matrixPhoneService) { // Check current URL to avoid to display the logout button on the login page $scope.location = $location.path(); @@ -36,6 +36,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even eventStreamService.resume(); mPresence.start(); } + + $scope.user_id; + var config = matrixService.config(); + if (config) { + $scope.user_id = matrixService.config().user_id; + } /** * Open a given page. @@ -45,6 +51,16 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even $location.url(url); }; + // Open the given user profile page + $scope.goToUserPage = function(user_id) { + if (user_id === $scope.user_id) { + $location.url("/settings"); + } + else { + $location.url("/user/" + user_id); + } + }; + // Logs the user out $scope.logout = function() { @@ -69,13 +85,30 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even $scope.logout(); }); - $scope.requestNotifications = function() { - if (window.Notification) { - console.log("Notification.permission: " + window.Notification.permission); - window.Notification.requestPermission(function(){}); - } + $scope.updateHeader = function() { + $scope.user_id = matrixService.config().user_id; + }; + + $rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) { + console.trace("incoming call"); + call.onError = $scope.onCallError; + call.onHangup = $scope.onCallHangup; + $rootScope.currentCall = call; + }); + + $scope.answerCall = function() { + $scope.currentCall.answer(); + }; + + $scope.hangupCall = function() { + $scope.currentCall.hangup(); + $scope.currentCall = undefined; }; -}]); + $rootScope.onCallError = function(errStr) { + $scope.feedback = errStr; + } - + $rootScope.onCallHangup = function() { + } +}]); diff --git a/webclient/app-directive.js b/webclient/app-directive.js index 01f60fdadf..eee0d3842f 100644 --- a/webclient/app-directive.js +++ b/webclient/app-directive.js @@ -32,7 +32,12 @@ angular.module('matrixWebClient') .directive('ngFocus', ['$timeout', function($timeout) { return { link: function(scope, element, attr) { - $timeout(function() { element[0].focus(); }, 0); + // XXX: slightly evil hack to disable autofocus on iOS, as in general + // it causes more problems than it fixes, by bouncing the page + // around + if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) { + $timeout(function() { element[0].focus(); }, 0); + } } }; }]); \ No newline at end of file diff --git a/webclient/app-filter.js b/webclient/app-filter.js index b8f4ed25bc..b8d3d2a0d8 100644 --- a/webclient/app-filter.js +++ b/webclient/app-filter.js @@ -70,7 +70,7 @@ angular.module('matrixWebClient') }); filtered.sort(function (a, b) { - return ((a["mtime_age"] || 10e10) > (b["mtime_age"] || 10e10) ? 1 : -1); + return ((a["last_active_ago"] || 10e10) > (b["last_active_ago"] || 10e10) ? 1 : -1); }); return filtered; }; @@ -79,4 +79,43 @@ angular.module('matrixWebClient') return function(text) { return $sce.trustAsHtml(text); }; +}]) + +// Compute the room name according to information we have +.filter('roomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) { + return function(room_id) { + var roomName; + + // If there is an alias, use it + // TODO: only one alias is managed for now + var alias = matrixService.getRoomIdToAliasMapping(room_id); + if (alias) { + roomName = alias; + } + + if (undefined === roomName) { + // Else, build the name from its users + var room = $rootScope.events.rooms[room_id]; + if (room) { + if (room.members) { + // Limit the room renaming to 1:1 room + if (2 === Object.keys(room.members).length) { + for (var i in room.members) { + var member = room.members[i]; + if (member.user_id !== matrixService.config().user_id) { + roomName = member.content.displayname ? member.content.displayname : member.user_id; + } + } + } + } + } + } + + if (undefined === roomName) { + // By default, use the room ID + roomName = room_id; + } + + return roomName; + }; }]); diff --git a/webclient/app.css b/webclient/app.css index 16f9dd72b7..c27ec797a4 100644..100755 --- a/webclient/app.css +++ b/webclient/app.css @@ -1,121 +1,198 @@ -/*** Mobile voodoo ***/ -@media all and (max-device-width: 640px) { - - #messageTableWrapper { - margin-right: 0px ! important; - } - - .leftBlock { - width: 8em ! important; - font-size: 8px ! important; - } - - .rightBlock { - width: 0px ! important; - display: none ! important; - } - - .avatar { - width: 36px ! important; - } - - #header, - #messageTable, - #wrapper, - #roomName, - #controls { - max-width: 640px ! important; - } - - #userIdCell, - #usersTableWrapper, - #extraControls { - display: none; - } - - #buttonsCell { - width: 60px ! important; - padding-left: 20px ! important; - } - - #roomLogo { - display: none; - } - - #roomName { - text-align: left ! important; - top: -35px ! important; - } - - .bubble { - font-size: 12px ! important; - min-height: 20px ! important; - } - - #page { - top: 35px ! important; - bottom: 70px ! important; - } - - #header, - #page { - margin: 5px ! important; - } - - #header { - padding: 5px ! important; - } - - /* stop zoom on select */ - select:focus, - textarea, - input - { - font-size: 16px ! important; - } - +/** Common layout **/ + +html { + height: 100%; } body { + height: 100%; font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif; font-size: 12pt; margin: 0px; } h1 { - font-family: Helvetica, Arial, sans-serif; + font-size: 20pt; } -/*** Overall page layout ***/ +a:link { color: #666; } +a:visited { color: #666; } +a:hover { color: #000; } +a:active { color: #000; } #page { - position: absolute; - top: 80px; - bottom: 100px; - left: 0px; - right: 0px; - margin: 20px; + min-height: 100%; + margin-bottom: -32px; /* to make room for the footer */ } #wrapper { margin: auto; max-width: 1280px; - height: 100%; + padding-top: 40px; + padding-bottom: 40px; + padding-left: 20px; + padding-right: 20px; } -#roomName { +#header +{ + position: absolute; + top: 0px; + width: 100%; + background-color: #333; + height: 32px; +} + +#callBar { + float: left; +} + +#headerContent { + color: #ccc; max-width: 1280px; + margin: auto; + text-align: right; + height: 32px; + line-height: 32px; +} + +#headerContent a:link, +#headerContent a:visited, +#headerContent a:hover, +#headerContent a:active { + color: #fff; +} + +#footer +{ width: 100%; + border-top: #666 1px solid; + background-color: #aaa; + height: 32px; +} + +#footerContent +{ + font-size: 8pt; + color: #fff; + max-width: 1280px; + margin: auto; + text-align: center; + height: 32px; + line-height: 32px; +} + +#genericHeading +{ + margin-top: 13px; +} + +#feedback { + color: #800; +} + +.mouse-pointer { + cursor: pointer; +} + +.invited { + opacity: 0.2; +} + +/*** Login Pages ***/ + +.loginWrapper { + text-align: center; +} + +#loginForm { + text-align: left; + padding: 1em; + margin-bottom: 40px; + display: inline-block; + + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + + -webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); + -moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); + box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); + + background-color: #f8f8f8; + border: 1px #ccc solid; +} + +#loginForm input[type='radio'] { + margin-right: 1em; +} + +#serverConfig { + text-align: center; +} + +#serverConfig, +#serverConfig input, +#serverConfig button +{ + font-size: 10pt ! important; +} + +.smallPrint { + color: #888; + font-size: 9pt ! important; + font-style: italic ! important; +} + +#serverConfig label { + display: inline-block; text-align: right; - top: -40px; + margin-right: 0.5em; + width: 7em; +} + +#loginForm, +#loginForm input, +#loginForm button, +#loginForm select { + font-size: 18px; +} + +/*** Room page ***/ + +#roomPage { position: absolute; + top: 120px; + bottom: 120px; + left: 20px; + right: 20px; +} + +#roomWrapper { + margin: auto; + max-width: 1280px; + height: 100%; +} + +#roomName { + float: right; font-size: 16px; + margin-top: 15px; +} + +#roomHeader { + margin: auto; + padding-left: 20px; + padding-right: 20px; + padding-top: 53px; + max-width: 1280px; } #controlPanel { position: absolute; bottom: 0px; width: 100%; + height: 100px; background-color: #f8f8f8; border-top: #aaa 1px solid; } @@ -146,10 +223,6 @@ h1 { background-color: #faa; } -.mouse-pointer { - cursor: pointer; -} - /*** Participant list ***/ #usersTableWrapper { @@ -195,6 +268,13 @@ h1 { word-break: break-all; } +.userAvatar .userPowerLevel { + position: absolute; + bottom: 20px; + height: 1px; + background-color: red; +} + .userPresence { text-align: center; font-size: 12px; @@ -300,7 +380,7 @@ h1 { display: inline-block; margin-bottom: -1px; max-width: 90%; - font-size: 16px; + font-size: 14px; word-wrap: break-word; padding-top: 7px; padding-bottom: 5px; @@ -310,6 +390,11 @@ h1 { -webkit-text-size-adjust:100% } +.bubble img { + max-width: 100%; + max-height: auto; +} + .differentUser td { padding-bottom: 5px ! important; } @@ -341,8 +426,8 @@ h1 { } #room-fullscreen-image img { - max-width: 100%; - max-height: 100%; + max-width: 90%; + max-height: 90%; bottom: 0; left: 0; margin: auto; @@ -350,9 +435,14 @@ h1 { position: fixed; right: 0; top: 0; + + -webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75); + box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75); } /*** Recents ***/ + .recentsTable { max-width: 480px; width: 100%; @@ -402,11 +492,14 @@ h1 { } /*** Recents in the room page ***/ + #roomRecentsTableWrapper { float: left; max-width: 320px; - margin-right: 20px; + padding-right: 10px; + margin-right: 10px; height: 100%; + border-right: 1px solid #ddd; overflow-y: auto; } @@ -421,55 +514,14 @@ h1 { } .profile-avatar img { - max-width: 100%; - max-height: 100%; + width: 100%; + height: 100%; + object-fit: cover; } /*** User profile page ***/ -#user-ids { - padding-left: 1em; -} #user-displayname { font-size: 24px; } -/******************************/ -#header -{ - padding: 20px; - max-width: 1280px; - margin: auto; -} - -#logo, -#roomLogo { - max-width: 1280px; - margin: auto; -} - -#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; -} diff --git a/webclient/app.js b/webclient/app.js index 02695c3ae6..dac4f048cd 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -18,6 +18,7 @@ var matrixWebClient = angular.module('matrixWebClient', [ 'ngRoute', 'MatrixWebClientController', 'LoginController', + 'RegisterController', 'RoomController', 'HomeController', 'RecentsController', @@ -35,30 +36,27 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', function($routeProvider, $provide, $httpProvider) { $routeProvider. when('/login', { - templateUrl: 'login/login.html', - controller: 'LoginController' + templateUrl: 'login/login.html' + }). + when('/register', { + templateUrl: 'login/register.html' }). when('/room/:room_id_or_alias', { - templateUrl: 'room/room.html', - controller: 'RoomController' + templateUrl: 'room/room.html' }). when('/room/', { // room URL with room alias in it (ex: http://127.0.0.1:8000/#/room/#public:localhost:8080) will come here. // The reason is that 2nd hash key breaks routeProvider parameters cutting so that the URL will not match with // the previous '/room/:room_id_or_alias' URL rule - templateUrl: 'room/room.html', - controller: 'RoomController' + templateUrl: 'room/room.html' }). when('/', { - templateUrl: 'home/home.html', - controller: 'HomeController' + templateUrl: 'home/home.html' }). when('/settings', { - templateUrl: 'settings/settings.html', - controller: 'SettingsController' + templateUrl: 'settings/settings.html' }). when('/user/:user_matrix_id', { - templateUrl: 'user/user.html', - controller: 'UserController' + templateUrl: 'user/user.html' }). otherwise({ redirectTo: '/' @@ -84,7 +82,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) { // If user auth details are not in cache, go to the login page - if (!matrixService.isUserLoggedIn()) { + if (!matrixService.isUserLoggedIn() && + $location.path() !== "/login" && + $location.path() !== "/register") + { $location.path("login"); } diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js index 5f01478fd1..699a3cbffc 100644 --- a/webclient/components/fileUpload/file-upload-service.js +++ b/webclient/components/fileUpload/file-upload-service.js @@ -30,7 +30,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities']) */ this.uploadFile = function(file) { var deferred = $q.defer(); - console.log("Uploading " + file.name + "... to /matrix/content"); + console.log("Uploading " + file.name + "... to /_matrix/content"); matrixService.uploadContent(file).then( function(response) { var content_url = response.data.content_token; diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js index b6e5c2eaac..d6a0600132 100644 --- a/webclient/components/matrix/event-handler-service.js +++ b/webclient/components/matrix/event-handler-service.js @@ -28,6 +28,7 @@ if typically all the $on method would do is update its own $scope. */ angular.module('eventHandlerService', []) .factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) { + var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT"; var MSG_EVENT = "MSG_EVENT"; var MEMBER_EVENT = "MEMBER_EVENT"; var PRESENCE_EVENT = "PRESENCE_EVENT"; @@ -48,7 +49,7 @@ angular.module('eventHandlerService', []) $rootScope.events.rooms[room_id].messages = []; $rootScope.events.rooms[room_id].members = {}; } - } + }; var resetRoomMessages = function(room_id) { if ($rootScope.events.rooms[room_id]) { @@ -56,6 +57,13 @@ angular.module('eventHandlerService', []) } }; + var handleRoomCreate = function(event, isLiveEvent) { + initRoom(event.room_id); + + // For now, we do not use the event data. Simply signal it to the app controllers + $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent); + }; + var handleMessage = function(event, isLiveEvent) { initRoom(event.room_id); @@ -95,12 +103,22 @@ angular.module('eventHandlerService', []) $rootScope.presence[event.content.user_id] = event; $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent); }; + + var handlePowerLevels = function(event, isLiveEvent) { + initRoom(event.room_id); + + $rootScope.events.rooms[event.room_id][event.type] = event; + + //TODO + //$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent); + }; var handleCallEvent = function(event, isLiveEvent) { $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent); }; return { + ROOM_CREATE_EVENT: ROOM_CREATE_EVENT, MSG_EVENT: MSG_EVENT, MEMBER_EVENT: MEMBER_EVENT, PRESENCE_EVENT: PRESENCE_EVENT, @@ -109,6 +127,9 @@ angular.module('eventHandlerService', []) handleEvent: function(event, isLiveEvent) { switch(event.type) { + case "m.room.create": + handleRoomCreate(event, isLiveEvent); + break; case "m.room.message": handleMessage(event, isLiveEvent); break; @@ -118,11 +139,20 @@ angular.module('eventHandlerService', []) case "m.presence": handlePresence(event, isLiveEvent); break; + case 'm.room.ops_levels': + case 'm.room.send_event_level': + case 'm.room.add_state_level': + case 'm.room.join_rules': + case 'm.room.power_levels': + handlePowerLevels(event, isLiveEvent); + break; + default: console.log("Unable to handle event type " + event.type); + console.log(JSON.stringify(event, undefined, 4)); break; } - if (event.type.indexOf('m.call.') == 0) { + if (event.type.indexOf('m.call.') === 0) { handleCallEvent(event, isLiveEvent); } }, diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index a66c879065..47b63d7f2f 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -73,9 +73,7 @@ angular.module('MatrixCall', []) this.state = 'wait_local_media'; }; - MatrixCall.prototype.hangup = function() { - console.trace("Ending call "+this.call_id); - + MatrixCall.prototype.stopAllMedia = function() { if (this.localAVStream) { forAllTracksOnStream(this.localAVStream, function(t) { t.stop(); @@ -86,6 +84,12 @@ angular.module('MatrixCall', []) t.stop(); }); } + }; + + MatrixCall.prototype.hangup = function() { + console.trace("Ending call "+this.call_id); + + this.stopAllMedia(); var content = { version: 0, @@ -233,8 +237,9 @@ angular.module('MatrixCall', []) t.onstarted = self.onRemoteStreamTrackStarted; }); + event.stream.onended = function(e) { self.onRemoteStreamEnded(e); }; // not currently implemented in chrome - event.stream.onstarted = this.onRemoteStreamStarted; + event.stream.onstarted = function(e) { self.onRemoteStreamStarted(e); }; var player = new Audio(); player.src = URL.createObjectURL(s); player.play(); @@ -244,24 +249,19 @@ angular.module('MatrixCall', []) this.state = 'connected'; }; + MatrixCall.prototype.onRemoteStreamEnded = function(event) { + this.state = 'ended'; + this.stopAllMedia(); + this.onHangup(); + }; + MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) { this.state = 'connected'; }; MatrixCall.prototype.onHangupReceived = function() { this.state = 'ended'; - - if (this.localAVStream) { - forAllTracksOnStream(this.localAVStream, function(t) { - t.stop(); - }); - } - if (this.remoteAVStream) { - forAllTracksOnStream(this.remoteAVStream, function(t) { - t.stop(); - }); - } - + this.stopAllMedia(); this.onHangup(); }; diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 8543491dca..2ae55bea9f 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -38,7 +38,7 @@ angular.module('matrixService', []) // Current version of permanent storage var configVersion = 0; - var prefixPath = "/matrix/client/api/v1"; + var prefixPath = "/_matrix/client/api/v1"; var MAPPING_PREFIX = "alias_for_"; var doRequest = function(method, path, params, data, $httpParams) { @@ -95,14 +95,18 @@ angular.module('matrixService', []) }, // Create a room - create: function(room_id, visibility) { + create: function(room_alias, visibility) { // The REST path spec var path = "/createRoom"; - return doRequest("POST", path, undefined, { - visibility: visibility, - room_alias_name: room_id - }); + var req = { + "visibility": visibility + }; + if (room_alias) { + req.room_alias_name = room_alias; + } + + return doRequest("POST", path, undefined, req); }, // List all rooms joined or been invited to @@ -164,7 +168,7 @@ angular.module('matrixService', []) // Retrieves the room ID corresponding to a room alias resolveRoomAlias:function(room_alias) { - var path = "/matrix/client/api/v1/directory/room/$room_alias"; + var path = "/_matrix/client/api/v1/directory/room/$room_alias"; room_alias = encodeURIComponent(room_alias); path = path.replace("$room_alias", room_alias); @@ -304,7 +308,7 @@ angular.module('matrixService', []) // hit the Identity Server for a 3PID request. linkEmail: function(email, clientSecret, sendAttempt) { - var path = "/matrix/identity/api/v1/validate/email/requestToken" + var path = "/_matrix/identity/api/v1/validate/email/requestToken" var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt; var headers = {}; headers["Content-Type"] = "application/x-www-form-urlencoded"; @@ -312,7 +316,7 @@ angular.module('matrixService', []) }, authEmail: function(clientSecret, tokenId, code) { - var path = "/matrix/identity/api/v1/validate/email/submitToken"; + var path = "/_matrix/identity/api/v1/validate/email/submitToken"; var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret; var headers = {}; headers["Content-Type"] = "application/x-www-form-urlencoded"; @@ -320,7 +324,7 @@ angular.module('matrixService', []) }, bindEmail: function(userId, tokenId, clientSecret) { - var path = "/matrix/identity/api/v1/3pid/bind"; + var path = "/_matrix/identity/api/v1/3pid/bind"; var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret; var headers = {}; headers["Content-Type"] = "application/x-www-form-urlencoded"; @@ -328,7 +332,7 @@ angular.module('matrixService', []) }, uploadContent: function(file) { - var path = "/matrix/content"; + var path = "/_matrix/content"; var headers = { "Content-Type": undefined // undefined means angular will figure it out }; @@ -480,6 +484,30 @@ angular.module('matrixService', []) getRoomIdToAliasMapping: function(roomId) { return localStorage.getItem(MAPPING_PREFIX+roomId); + }, + + + /****** Power levels management ******/ + + /** + * Return the power level of an user in a particular room + * @param {String} room_id the room id + * @param {String} user_id the user id + * @returns {Number} a value between 0 and 10 + */ + getUserPowerLevel: function(room_id, user_id) { + var powerLevel = 0; + var room = $rootScope.events.rooms[room_id]; + if (room && room["m.room.power_levels"]) { + if (user_id in room["m.room.power_levels"].content) { + powerLevel = room["m.room.power_levels"].content[user_id]; + } + else { + // Use the room default user power + powerLevel = room["m.room.power_levels"].content["default"]; + } + } + return powerLevel; } }; diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js index 547a5c5603..f4ce3053ea 100644 --- a/webclient/home/home-controller.js +++ b/webclient/home/home-controller.js @@ -17,8 +17,8 @@ limitations under the License. 'use strict'; angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController']) -.controller('HomeController', ['$scope', '$location', 'matrixService', - function($scope, $location, matrixService) { +.controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService', + function($scope, $location, matrixService, eventHandlerService) { $scope.config = matrixService.config(); $scope.public_rooms = []; @@ -37,6 +37,11 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen $scope.joinAlias = { room_alias: "" }; + + $scope.profile = { + displayName: "", + avatarUrl: "" + }; var refresh = function() { @@ -53,21 +58,20 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen ); }; - $scope.createNewRoom = function(room_id, isPrivate) { + $scope.createNewRoom = function(room_alias, isPrivate) { var visibility = "public"; if (isPrivate) { visibility = "private"; } - matrixService.create(room_id, visibility).then( + matrixService.create(room_alias, visibility).then( function(response) { // This room has been created. Refresh the rooms list console.log("Created room " + response.data.room_alias + " with id: "+ response.data.room_id); matrixService.createRoomIdToAliasMapping( response.data.room_id, response.data.room_alias); - refresh(); }, function(error) { $scope.feedback = "Failure: " + error.data; @@ -108,6 +112,34 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen }; $scope.onInit = function() { + // Load profile data + // Display name + matrixService.getDisplayName($scope.config.user_id).then( + function(response) { + $scope.profile.displayName = response.data.displayname; + }, + function(error) { + $scope.feedback = "Can't load display name"; + } + ); + // Avatar + matrixService.getProfilePictureUrl($scope.config.user_id).then( + function(response) { + $scope.profile.avatarUrl = response.data.avatar_url; + }, + function(error) { + $scope.feedback = "Can't load avatar URL"; + } + ); + + // Listen to room creation event in order to update the public rooms list + $scope.$on(eventHandlerService.ROOM_CREATE_EVENT, function(ngEvent, event, isLive) { + if (isLive) { + // As we do not know if this room is public, do a full list refresh + refresh(); + } + }); + refresh(); }; }]); diff --git a/webclient/home/home.html b/webclient/home/home.html index d38b843d83..c1f9643839 100644 --- a/webclient/home/home.html +++ b/webclient/home/home.html @@ -1,29 +1,24 @@ <div ng-controller="HomeController" data-ng-init="onInit()"> - <div id="page"> <div id="wrapper"> - + + <div id="genericHeading"> + <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> + </div> + + <h1>Welcome to homeserver {{ config.homeserver }}</h1> + <div> - <form> - <table> - <tr> - <td> - <div class="profile-avatar"> - <img ng-src="{{ config.avatarUrl || 'img/default-profile.jpg' }}"/> - </div> - </td> - <td> - <div id="user-ids"> - <div id="user-displayname">{{ config.displayName }}</div> - <div>{{ config.user_id }}</div> - </div> - </td> - </tr> - </table> - </form> + <div class="profile-avatar"> + <img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}"/> + </div> + <div id="user-ids"> + <div id="user-displayname">{{ profile.displayName }}</div> + <div>{{ config.user_id }}</div> + </div> </div> - <h3>Recents</h3> + <h3>Recent conversations</h3> <div ng-include="'recents/recents.html'"></div> <br/> @@ -38,9 +33,9 @@ <div> <form> - <input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/> + <input size="40" ng-model="newRoom.room_alias" ng-enter="createNewRoom(newRoom.room_alias, 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> + <button ng-disabled="!newRoom.room_alias" ng-click="createNewRoom(newRoom.room_alias, newRoom.private)">Create room</button> </form> </div> <div> @@ -54,5 +49,4 @@ {{ feedback }} </div> - </div> </div> diff --git a/webclient/img/default-profile.jpg b/webclient/img/default-profile.jpg deleted file mode 100644 index 20f2a2b085..0000000000 --- a/webclient/img/default-profile.jpg +++ /dev/null Binary files differdiff --git a/webclient/img/default-profile.png b/webclient/img/default-profile.png new file mode 100644 index 0000000000..6f81a3c417 --- /dev/null +++ b/webclient/img/default-profile.png Binary files differdiff --git a/webclient/img/logo-small.png b/webclient/img/logo-small.png new file mode 100644 index 0000000000..411206dcdc --- /dev/null +++ b/webclient/img/logo-small.png Binary files differdiff --git a/webclient/img/logo.png b/webclient/img/logo.png new file mode 100644 index 0000000000..c4b53a8487 --- /dev/null +++ b/webclient/img/logo.png Binary files differdiff --git a/webclient/index.html b/webclient/index.html index 5faf165626..f016dbb877 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -4,6 +4,8 @@ <title>[matrix]</title> <link rel="stylesheet" href="app.css"> + <link rel="stylesheet" href="mobile.css"> + <link rel="icon" href="favicon.ico"> <meta name="viewport" content="width=device-width"> @@ -19,6 +21,7 @@ <script src="app-filter.js"></script> <script src="home/home-controller.js"></script> <script src="login/login-controller.js"></script> + <script src="login/register-controller.js"></script> <script src="recents/recents-controller.js"></script> <script src="recents/recents-filter.js"></script> <script src="room/room-controller.js"></script> @@ -38,15 +41,36 @@ <body> - <header id="header"> + <div id="header"> <!-- Do not show buttons on the login page --> - <div id="header-buttons" ng-hide="'/login' == location "> + <div id="headerContent" ng-hide="'/login' == location || '/register' == location"> + <div id="callBar"> + <div ng-show="currentCall.state == 'ringing'"> + Incoming call from {{ currentCall.user_id }} + <button ng-click="answerCall()">Answer</button> + <button ng-click="hangupCall()">Reject</button> + </div> + <button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button> + <span ng-show="currentCall.state == 'invite_sent'">Calling...</span> + <span ng-show="currentCall.state == 'connecting'">Call Connecting...</span> + <span ng-show="currentCall.state == 'connected'">Call Connected</span> + <span ng-show="currentCall.state == 'ended'">Call Ended</span> + <span style="display: none; ">{{ currentCall.state }}</span> + </div> + <a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a> + + <button ng-click='goToPage("/")'>Home</button> <button ng-click='goToPage("settings")'>Settings</button> <button ng-click="logout()">Log out</button> </div> - </header> + </div> - <div ng-view></div> + <div id="page" ng-view></div> + <div id="footer" ng-hide="location.indexOf('/room') == 0"> + <div id="footerContent"> + © 2014 Matrix.org + </div> + </div> </body> </html> diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js index 51f9a3bdf4..7369a28ef0 100644 --- a/webclient/login/login-controller.js +++ b/webclient/login/login-controller.js @@ -1,3 +1,19 @@ +/* + Copyright 2014 matrix.org + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + angular.module('LoginController', ['matrixService']) .controller('LoginController', ['$scope', '$location', 'matrixService', 'eventStreamService', function($scope, $location, matrixService, eventStreamService) { @@ -7,7 +23,10 @@ angular.module('LoginController', ['matrixService']) // Assume that this is hosted on the home server, in which case the URL // contains the home server. var hs_url = $location.protocol() + "://" + $location.host(); - if ($location.port()) { + if ($location.port() && + !($location.protocol() === "http" && $location.port() === 80) && + !($location.protocol() === "https" && $location.port() === 443)) + { hs_url += ":" + $location.port(); } @@ -16,57 +35,18 @@ angular.module('LoginController', ['matrixService']) desired_user_name: "", user_id: "", password: "", - identityServer: "", + identityServer: "http://matrix.org:8090", pwd1: "", - pwd2: "" + 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(response) { - $scope.feedback = "Success"; - // Update the current config - var config = matrixService.config(); - angular.extend(config, { - access_token: response.data.access_token, - user_id: response.data.user_id - }); - matrixService.setConfig(config); - - // And permanently save it - matrixService.saveConfig(); - eventStreamService.resume(); - // Go to the user's rooms list page - $location.url("home"); - }, - function(error) { - if (error.data) { - if (error.data.errcode === "M_USER_IN_USE") { - $scope.feedback = "Username already taken."; - } - } - else if (error.status === 0) { - $scope.feedback = "Unable to talk to the server."; - } - }); + + $scope.login_types = [ "email", "mxid" ]; + $scope.login_type_label = { + "email": "Email address", + "mxid": "Matrix ID (e.g. @bob:matrix.org or bob)", }; - + $scope.login_type = 'mxid'; // TODO: remember the user's preferred login_type + $scope.login = function() { matrixService.setConfig({ homeserver: $scope.account.homeserver, diff --git a/webclient/login/login.html b/webclient/login/login.html index 4b2ea60928..18e7a02815 100644 --- a/webclient/login/login.html +++ b/webclient/login/login.html @@ -1,55 +1,49 @@ <div ng-controller="LoginController" class="login"> - <h1 id="logo">[matrix]</h1> - - <div id="page"> - <div id="wrapper"> - - {{ feedback }} + <div id="wrapper" class="loginWrapper"> - <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"/> + <a href ng-click="goToPage('/')"> + <img src="img/logo.png" width="240" height="102" alt="[matrix]" style="padding: 50px"/> + </a> + <br/> - <!-- New user registration --> - <div> - <br/> - <button ng-click="register()" ng-disabled="!account.desired_user_name || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Register</button> - </div> - </form> - <h3>Got an account?</h3> - <form novalidate> - <!-- Login with an registered user --> - <div>{{ login_error_msg }} </div> - <div> - <input id="user_id" size="70" type="text" auto-focus ng-model="account.user_id" placeholder="User ID (ex:@bob:localhost or bob)"/> - <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">Login</button> - </div> - - </form> + <form id="loginForm" novalidate> + <!-- Login with an registered user --> + <div> + Log in using:<br/> + + <div ng-repeat="type in login_types"> + <input type="radio" ng-model="$parent.login_type" value="{{ type }}" id="radio_{{ type }}"/> + <label for="radio_{{ type }}">{{ login_type_label[type] }}</label> + </div> + + <div style="text-align: center"> + <br/> + <input id="user_id" size="32" type="text" ng-focus="true" ng-model="account.user_id" placeholder="{{ login_type_label[login_type] }}"/> + <br/> + <input id="password" size="32" type="password" ng-model="account.password" placeholder="Password"/> + <br/><br/> + <button ng-click="login()" ng-disabled="!account.user_id || !account.password || !account.homeserver">Login</button> + <br/><br/> + </div> - <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 class="feedback">{{ feedback }} {{ login_error_msg }}</div> + + <div id="serverConfig"> + <label for="homeserver">Home Server:</label> + <input id="homeserver" size="32" type="text" ng-model="account.homeserver" placeholder="URL (e.g. http://matrix.org:8080)"/> + <div class="smallPrint">Your home server stores all your conversation and account data.</div> + <label for="identityServer">Identity Server:</label> + <input id="identityServer" size="32" type="text" ng-model="account.identityServer" placeholder="URL (e.g. http://matrix.org:8090)"/> + <div class="smallPrint">Matrix provides identity servers to track which emails etc. belong to which Matrix IDs.<br/> + Only http://matrix.org:8090 currently exists.</div> + <br/> + <br/> + <a href="#/register" style="padding-right: 3em">Create account</a> + <a href="#/reset_password">Forgotten password?</a> + </div> + </div> + </form> </div> </div> diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js new file mode 100644 index 0000000000..0ece57502b --- /dev/null +++ b/webclient/login/register-controller.js @@ -0,0 +1,102 @@ +/* + Copyright 2014 matrix.org + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +angular.module('RegisterController', ['matrixService']) +.controller('RegisterController', ['$scope', '$location', 'matrixService', 'eventStreamService', + function($scope, $location, matrixService, eventStreamService) { + 'use strict'; + + // FIXME: factor out duplication with login-controller.js + + // Assume that this is hosted on the home server, in which case the URL + // contains the home server. + var hs_url = $location.protocol() + "://" + $location.host(); + if ($location.port() && + !($location.protocol() === "http" && $location.port() === 80) && + !($location.protocol() === "https" && $location.port() === 443)) + { + hs_url += ":" + $location.port(); + } + + $scope.account = { + homeserver: hs_url, + desired_user_id: "", + desired_user_name: "", + password: "", + identityServer: "http://matrix.org:8090", + pwd1: "", + pwd2: "", + displayName : "" + }; + + $scope.register = function() { + + // Set the urls + matrixService.setConfig({ + homeserver: $scope.account.homeserver, + identityServer: $scope.account.identityServer + }); + + if ($scope.account.pwd1 !== $scope.account.pwd2) { + $scope.feedback = "Passwords don't match."; + return; + } + else if ($scope.account.pwd1.length < 6) { + $scope.feedback = "Password must be at least 6 characters."; + return; + } + + matrixService.register($scope.account.desired_user_id, $scope.account.pwd1).then( + function(response) { + $scope.feedback = "Success"; + // Update the current config + var config = matrixService.config(); + angular.extend(config, { + access_token: response.data.access_token, + user_id: response.data.user_id + }); + matrixService.setConfig(config); + + // And permanently save it + matrixService.saveConfig(); + + // Update the global scoped used_id var (used in the app header) + $scope.updateHeader(); + + eventStreamService.resume(); + + if ($scope.account.displayName) { + // FIXME: handle errors setting displayName + matrixService.setDisplayName($scope.account.displayName); + } + + // Go to the user's rooms list page + $location.url("home"); + }, + function(error) { + if (error.data) { + if (error.data.errcode === "M_USER_IN_USE") { + $scope.feedback = "Username already taken."; + } + } + else if (error.status === 0) { + $scope.feedback = "Unable to talk to the server."; + } + }); + }; + +}]); + diff --git a/webclient/login/register.html b/webclient/login/register.html new file mode 100644 index 0000000000..1b470e4554 --- /dev/null +++ b/webclient/login/register.html @@ -0,0 +1,48 @@ +<div ng-controller="RegisterController" class="register"> + <div id="wrapper" class="loginWrapper"> + + <a href ng-click="goToPage('/')"> + <img src="img/logo.png" width="240" height="102" alt="[matrix]" style="padding: 50px"/> + </a> + <br/> + + <form id="loginForm" novalidate> + <div> + Create account:<br/> + + <div style="text-align: center"> + <br/> + <input id="email" size="32" type="text" ng-focus="true" ng-model="account.email" placeholder="Email address (optional)"/> + <div class="smallPrint">Specifying an email address lets other users find you on Matrix more easily,<br/> + and gives you a way to reset your password</div> + <input id="desired_user_id" size="32" type="text" ng-model="account.desired_user_id" placeholder="Matrix ID (e.g. bob)"/> + <br/> + <input id="pwd1" size="32" type="password" ng-model="account.pwd1" placeholder="Type a password"/> + <br/> + <input id="pwd2" size="32" type="password" ng-model="account.pwd2" placeholder="Confirm your password"/> + <br/> + <input id="displayName" size="32" type="text" ng-model="account.displayName" placeholder="Display name (e.g. Bob Obson)"/> + <br/> + <br/> + + <button ng-click="register()" ng-disabled="!account.desired_user_id || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Sign up</button> + <br/><br/> + </div> + + <div class="feedback">{{ feedback }} {{ login_error_msg }}</div> + + <div id="serverConfig"> + <label for="homeserver">Home Server:</label> + <input id="homeserver" size="32" type="text" ng-model="account.homeserver" placeholder="URL (e.g. http://matrix.org:8080)"/> + <div class="smallPrint">Your home server stores all your conversation and account data.</div> + <label for="identityServer">Identity Server:</label> + <input id="identityServer" size="32" type="text" ng-model="account.identityServer" placeholder="URL (e.g. http://matrix.org:8090)"/> + <div class="smallPrint">Matrix provides identity servers to track which emails etc. belong to which Matrix IDs.<br/> + Only http://matrix.org:8090 currently exists.</div> + </div> + </div> + </form> + + </div> + </div> +</div> diff --git a/webclient/mobile.css b/webclient/mobile.css new file mode 100644 index 0000000000..7c62a072d5 --- /dev/null +++ b/webclient/mobile.css @@ -0,0 +1,92 @@ +/*** Mobile voodoo ***/ +@media all and (max-device-width: 640px) { + + #messageTableWrapper { + margin-right: 0px ! important; + } + + .leftBlock { + width: 8em ! important; + font-size: 8px ! important; + } + + .rightBlock { + width: 0px ! important; + display: none ! important; + } + + .avatar { + width: 36px ! important; + } + + #header { + background-color: transparent; + } + + #headerContent { + padding-right: 5px; + } + + #headerContent button { + font-size: 8px; + } + + #messageTable, + #wrapper, + #controls { + max-width: 640px ! important; + } + + #headerUserId, + #roomHeader img, + #userIdCell, + #roomRecentsTableWrapper, + #usersTableWrapper, + .extraControls { + display: none; + } + + #buttonsCell { + width: 60px ! important; + padding-left: 20px ! important; + } + + #roomLogo { + display: none; + } + + .bubble { + font-size: 12px ! important; + min-height: 20px ! important; + } + + #roomHeader { + padding-top: 10px; + } + + #roomName { + float: left; + font-size: 14px ! important; + margin-top: 0px ! important; + } + + #roomPage { + top: 35px ! important; + left: 5px ! important; + right: 5px ! important; + bottom: 70px ! important; + } + + #controlPanel { + height: 70px; + } + + /* stop zoom on select */ + select:focus, + textarea, + input + { + font-size: 16px ! important; + } + +} diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js index d33d41a922..d7d3bf4053 100644 --- a/webclient/recents/recents-controller.js +++ b/webclient/recents/recents-controller.js @@ -33,8 +33,7 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService']) console.log("Invited to room " + event.room_id); // FIXME push membership to top level key to match /im/sync event.membership = event.content.membership; - // FIXME bodge a nicer name than the room ID for this invite. - event.room_display_name = event.user_id + "'s room"; + $scope.rooms[event.room_id] = event; } }); @@ -43,6 +42,16 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService']) $scope.rooms[event.room_id].lastMsg = event; } }); + $scope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) { + if (isLive) { + $scope.rooms[event.room_id].lastMsg = event; + } + }); + $scope.$on(eventHandlerService.ROOM_CREATE_EVENT, function(ngEvent, event, isLive) { + if (isLive) { + $scope.rooms[event.room_id] = event; + } + }); }; @@ -83,7 +92,9 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService']) }; $scope.onInit = function() { - refresh(); + eventHandlerService.waitForInitialSyncCompletion().then(function() { + refresh(); + }); }; }]); diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html index 3f025a98d8..db3b0fb32f 100644 --- a/webclient/recents/recents.html +++ b/webclient/recents/recents.html @@ -6,7 +6,7 @@ ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}"> <tr> <td class="recentsRoomName"> - {{ room.room_display_name }} + {{ room.room_id | roomName }} </td> <td class="recentsRoomSummaryTS"> {{ (room.lastMsg.ts) | date:'MMM d HH:mm' }} @@ -51,7 +51,9 @@ </div> <div ng-switch-default> - {{ room.lastMsg }} + <div ng-if="room.lastMsg.type.indexOf('m.call.') == 0"> + Call + </div> </div> </div> </td> diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index ca0d0bae75..1f90472c67 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -75,18 +75,15 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) }); $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) { - updateMemberList(event); + if (isLive) { + updateMemberList(event); + } }); $scope.$on(eventHandlerService.PRESENCE_EVENT, function(ngEvent, event, isLive) { - updatePresence(event); - }); - - $rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) { - console.trace("incoming call"); - call.onError = $scope.onCallError; - call.onHangup = $scope.onCallHangup; - $scope.currentCall = call; + if (isLive) { + updatePresence(event); + } }); $scope.memberCount = function() { @@ -100,15 +97,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) } }; - $scope.answerCall = function() { - $scope.currentCall.answer(); - }; - - $scope.hangupCall = function() { - $scope.currentCall.hangup(); - $scope.currentCall = undefined; - }; - var paginate = function(numItems) { // console.log("paginate " + numItems); if ($scope.state.paginating || !$scope.room_id) { @@ -175,65 +163,49 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) // set target_user_id to keep things clear var target_user_id = chunk.state_key; + + var now = new Date().getTime(); var isNewMember = !(target_user_id in $scope.members); if (isNewMember) { // FIXME: why are we copying these fields around inside chunk? - if ("state" in chunk.content) { - chunk.presenceState = chunk.content.state; // why is this renamed? + if ("presence" in chunk.content) { + chunk.presence = chunk.content.presence; } - if ("mtime_age" in chunk.content) { - chunk.mtime_age = chunk.content.mtime_age; + if ("last_active_ago" in chunk.content) { + chunk.last_active_ago = chunk.content.last_active_ago; } - // Once the HS reliably returns the displaynames & avatar_urls for both - // local and remote users, we should use this rather than the evalAsync block - // below if ("displayname" in chunk.content) { chunk.displayname = chunk.content.displayname; } if ("avatar_url" in chunk.content) { chunk.avatar_url = chunk.content.avatar_url; } - $scope.members[target_user_id] = chunk; - -/* - // Stale code for explicitly hammering the homeserver for every displayname & avatar_url - - // 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) { - member.displayname = response.data.displayname; - } - } - ); - matrixService.getProfilePictureUrl(chunk.target_user_id).then( - function(response) { - var member = $scope.members[chunk.target_user_id]; - if (member !== undefined) { - member.avatar_url = response.data.avatar_url; - } - } - ); - }); -*/ + chunk.last_updated = now; + $scope.members[target_user_id] = chunk; if (target_user_id in $rootScope.presence) { updatePresence($rootScope.presence[target_user_id]); } } else { - // selectively update membership else it will nuke the picture and displayname too :/ + // selectively update membership and presence else it will nuke the picture and displayname too :/ var member = $scope.members[target_user_id]; - member.content.membership = chunk.content.membership; + member.membership = chunk.content.membership; + if ("presence" in chunk.content) { + member.presence = chunk.content.presence; + } + if ("last_active_ago" in chunk.content) { + member.last_active_ago = chunk.content.last_active_ago; + } } }; + + var updateMemberListPresenceAge = function() { + $scope.now = new Date().getTime(); + // TODO: don't bother polling every 5s if we know none of our counters are younger than 1 minute + $timeout(updateMemberListPresenceAge, 5 * 1000); + }; var updatePresence = function(chunk) { if (!(chunk.content.user_id in $scope.members)) { @@ -243,13 +215,12 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) var member = $scope.members[chunk.content.user_id]; // XXX: why not just pass the chunk straight through? - if ("state" in chunk.content) { - member.presenceState = chunk.content.state; + if ("presence" in chunk.content) { + member.presence = chunk.content.presence; } - if ("mtime_age" in chunk.content) { - // FIXME: should probably keep updating mtime_age in realtime like FB does - member.mtime_age = chunk.content.mtime_age; + if ("last_active_ago" in chunk.content) { + member.last_active_ago = chunk.content.last_active_ago; } // this may also contain a new display name or avatar url, so check. @@ -262,6 +233,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) } }; + var updateUserPowerLevel = function(user_id) { + var member = $scope.members[user_id]; + if (member) { + member.powerLevel = matrixService.getUserPowerLevel($scope.room_id, user_id); + } + } + $scope.send = function() { if ($scope.textInput === "") { return; @@ -275,6 +253,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) if ($scope.textInput.indexOf("/me") === 0) { promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4)); } + else if ($scope.textInput.indexOf("/nick ") === 0) { + // Change user display name + promise = matrixService.setDisplayName($scope.textInput.substr(6)); + } else { promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput); } @@ -349,6 +331,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) // Make sure the initialSync has been before going further eventHandlerService.waitForInitialSyncCompletion().then( function() { + + // Some data has been retrieved from the iniialSync request + // So, the relative time starts here + $scope.now = new Date().getTime(); + var needsToJoin = true; // The room members is available in the data fetched by initialSync @@ -395,7 +382,26 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) // Make recents highlight the current room $scope.recentsSelectedRoomID = $scope.room_id; - + + // Get the up-to-date the current member list + matrixService.getMemberList($scope.room_id).then( + function(response) { + for (var i = 0; i < response.data.chunk.length; i++) { + var chunk = response.data.chunk[i]; + updateMemberList(chunk); + + // Add his power level + updateUserPowerLevel(chunk.user_id); + } + + // Arm list timing update timer + updateMemberListPresenceAge(); + }, + function(error) { + $scope.feedback = "Failed get member list: " + error.data.error; + } + ); + paginate(MESSAGES_PER_PAGINATION); }; @@ -404,18 +410,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) matrixService.invite($scope.room_id, user_id).then( function() { console.log("Invited."); - $scope.feedback = "Request for invitation succeeds"; + $scope.feedback = "Invite sent successfully"; }, function(reason) { $scope.feedback = "Failure: " + reason; }); }; - // Open the user profile page - $scope.goToUserPage = function(user_id) { - $location.url("/user/" + user_id); - }; - $scope.leaveRoom = function() { matrixService.leave($scope.room_id).then( @@ -476,16 +477,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) $scope.startVoiceCall = function() { var call = new MatrixCall($scope.room_id); - call.onError = $scope.onCallError; - call.onHangup = $scope.onCallHangup; + call.onError = $rootScope.onCallError; + call.onHangup = $rootScope.onCallHangup; call.placeCall(); - $scope.currentCall = call; + $rootScope.currentCall = call; } - $scope.onCallError = function(errStr) { - $scope.feedback = errStr; - } - - $scope.onCallHangup = function() { - } }]); diff --git a/webclient/room/room.html b/webclient/room/room.html index 262b5df82e..e672b1d7e2 100644 --- a/webclient/room/room.html +++ b/webclient/room/room.html @@ -1,13 +1,15 @@ -<div ng-controller="RoomController" data-ng-init="onInit()" class="room"> - <h1 id="roomLogo">[matrix]</h1> +<div ng-controller="RoomController" data-ng-init="onInit()" class="room" style="height: 100%;"> - <div id="page"> - <div id="wrapper"> - - <div id="roomName"> - {{ room_alias || room_id }} + <div id="roomHeader"> + <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> + <div id="roomName"> + {{ room_id | roomName }} + </div> </div> + <div id="roomPage"> + <div id="roomWrapper"> + <div id="roomRecentsTableWrapper"> <div ng-include="'recents/recents.html'"></div> </div> @@ -15,17 +17,18 @@ <div id="usersTableWrapper"> <table id="usersTable"> <tr ng-repeat="member in members | orderMembersList"> - <td class="userAvatar mouse-pointer" ng-click="goToUserPage(member.id)"> + <td class="userAvatar mouse-pointer" ng-click="$parent.goToUserPage(member.id)" ng-class="member.membership == 'invite' ? 'invited' : ''"> <img class="userAvatarImage" - ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}" + ng-src="{{member.avatar_url || 'img/default-profile.png'}}" alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}" title="{{ member.id }}" width="80" height="80"/> <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/> + <div class="userPowerLevel" ng-style="{'width': (10 * member.powerLevel) +'%'}"></div> <div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div> </td> - <td class="userPresence" ng-class="member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')"> - {{ member.mtime_age | duration }}<br/>{{ member.mtime_age ? "ago" : "" }} + <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')"> + <span ng-show="member.last_active_ago">{{ member.last_active_ago + (now - member.last_updated) | duration }}<br/>ago</span> </td> </table> </div> @@ -40,7 +43,7 @@ <div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div> </td> <td class="avatar"> - <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32" + <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/> </td> <td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'"> @@ -64,7 +67,7 @@ </div> </td> <td class="rightBlock"> - <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32" + <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/> </td> </tr> @@ -86,30 +89,19 @@ </td> <td id="buttonsCell"> <button ng-click="send()">Send</button> - <button m-file-input="imageFileToSend">Image</button> + <button m-file-input="imageFileToSend" class="extraControls">Image</button> </td> </tr> </table> - <div id="extraControls"> + <div class="extraControls"> <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> - <button ng-click="startVoiceCall()" ng-show="currentCall == undefined && memberCount() == 2">Voice Call</button> - <div ng-show="currentCall.state == 'ringing'"> - Incoming call from {{ currentCall.user_id }} - <button ng-click="answerCall()">Answer</button> - <button ng-click="hangupCall()">Reject</button> - </div> - <button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing'">Hang up</button> - <span ng-show="currentCall.state == 'invite_sent'">Calling...</span> - <span ng-show="currentCall.state == 'connecting'">Call Connecting...</span> - <span ng-show="currentCall.state == 'connected'">Call Connected</span> - <span ng-show="currentCall.state == 'ended'">Call Ended</span> - <span style="display: none; ">{{ currentCall.state }}</span> + <button ng-click="startVoiceCall()" ng-show="(currentCall == undefined || currentCall.state == 'ended') && memberCount() == 2">Voice Call</button> </div> {{ feedback }} diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js index f7d5e8eb75..dc680ef075 100644 --- a/webclient/settings/settings-controller.js +++ b/webclient/settings/settings-controller.js @@ -22,8 +22,38 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu $scope.config = matrixService.config(); $scope.profile = { - displayName: $scope.config.displayName, - avatarUrl: $scope.config.avatarUrl + displayName: "", + avatarUrl: "" + }; + + // The profile as stored on the server + $scope.profileOnServer = { + displayName: "", + avatarUrl: "" + }; + + $scope.onInit = function() { + // Load profile data + // Display name + matrixService.getDisplayName($scope.config.user_id).then( + function(response) { + $scope.profile.displayName = response.data.displayname; + $scope.profileOnServer.displayName = response.data.displayname; + }, + function(error) { + $scope.feedback = "Can't load display name"; + } + ); + // Avatar + matrixService.getProfilePictureUrl($scope.config.user_id).then( + function(response) { + $scope.profile.avatarUrl = response.data.avatar_url; + $scope.profileOnServer.avatarUrl = response.data.avatar_url; + }, + function(error) { + $scope.feedback = "Can't load avatar URL"; + } + ); }; $scope.$watch("profile.avatarFile", function(newValue, oldValue) { @@ -41,10 +71,10 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu }); $scope.saveProfile = function() { - if ($scope.profile.displayName !== $scope.config.displayName) { + if ($scope.profile.displayName !== $scope.profileOnServer.displayName) { setDisplayName($scope.profile.displayName); } - if ($scope.profile.avatarUrl !== $scope.config.avatarUrl) { + if ($scope.profile.avatarUrl !== $scope.profileOnServer.avatarUrl) { setAvatar($scope.profile.avatarUrl); } }; @@ -53,11 +83,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu matrixService.setDisplayName(displayName).then( function(response) { $scope.feedback = "Updated display name."; - - var config = matrixService.config(); - config.displayName = displayName; - matrixService.setConfig(config); - matrixService.saveConfig(); }, function(error) { $scope.feedback = "Can't update display name: " + error.data; @@ -71,11 +96,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu function(response) { console.log("Updated avatar"); $scope.feedback = "Updated avatar."; - - var config = matrixService.config(); - config.avatarUrl = avatarURL; - matrixService.setConfig(config); - matrixService.saveConfig(); }, function(error) { $scope.feedback = "Can't update avatar: " + error.data; @@ -143,4 +163,23 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu } ); }; + + + /*** Desktop notifications section ***/ + $scope.settings = { + notifications: undefined + }; + + // If the browser supports it, check the desktop notification state + if ("Notification" in window) { + $scope.settings.notifications = window.Notification.permission; + } + + $scope.requestNotifications = function() { + console.log("requestNotifications"); + window.Notification.requestPermission(function (permission) { + console.log(" -> User decision: " + permission); + $scope.settings.notifications = permission; + }); + }; }]); \ No newline at end of file diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html index 453a4fc35f..a69a8de300 100644 --- a/webclient/settings/settings.html +++ b/webclient/settings/settings.html @@ -1,35 +1,29 @@ -<div ng-controller="SettingsController" class="user"> +<div ng-controller="SettingsController" class="user" data-ng-init="onInit()"> - <div id="page"> <div id="wrapper"> - - <h3>Me</h3> - <div> + + <div id="genericHeading"> + <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> + </div> + + <h1>Settings</h1> + <div class="section"> <form> - <table> - <tr> - <td> - <div class="profile-avatar"> - <img ng-src="{{ profile.avatarUrl || 'img/default-profile.jpg' }}" m-file-input="profile.avatarFile"/> - </div> - </td> - <td> - <div id="user-ids"> - <input size="40" ng-model="profile.displayName" placeholder="Your name"/> - </div> - </td> - <td> - <button ng-disabled="(profile.displayName == config.displayName) && (profile.avatarUrl == config.avatarUrl)" - ng-click="saveProfile()">Save</button> - </td> - </tr> - </table> + <div class="profile-avatar"> + <img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}" m-file-input="profile.avatarFile"/> + </div> + <div id="user-ids"> + <input size="40" ng-model="profile.displayName" placeholder="Your display name"/> + <br/> + <button ng-disabled="(profile.displayName == profileOnServer.displayName) && (profile.avatarUrl == profileOnServer.avatarUrl)" + ng-click="saveProfile()">Save</button> + </div> </form> </div> <br/> <h3>Linked emails</h3> - <div> + <div class="section"> <form> <input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" /> <button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)"> @@ -52,22 +46,35 @@ </table> </div> <br/> - + + <h3>Desktop notifications</h3> + <div class="section" ng-switch="settings.notifications"> + <div ng-switch-when="granted"> + Notifications are enabled. + </div> + <div ng-switch-when="denied"> + You have denied permission for notifications.<br/> + To enable it, reset the notification setting for this web site into your browser settings. + </div> + <div ng-switch-when="default"> + <button ng-click="requestNotifications()" style="font-size: 14pt">Enable desktop notifications</button> + </div> + <div ng-switch-default=""> + Sorry, your browser does not support notifications. + </div> + </div> + <br/> + <h3>Configuration</h3> - <div> + <div class="section"> <div>Home server: {{ config.homeserver }} </div> + <div>Identity server: {{ config.identityServer }} </div> <div>User ID: {{ config.user_id }} </div> <div>Access token: {{ config.access_token }} </div> </div> <br/> - - <div> - <div><button ng-click="requestNotifications()">Request notifications</button></div> - </div> - <br/> {{ feedback }} </div> - </div> </div> diff --git a/webclient/user/user-controller.js b/webclient/user/user-controller.js index 620230561c..b5b2d439a2 100644 --- a/webclient/user/user-controller.js +++ b/webclient/user/user-controller.js @@ -25,14 +25,42 @@ angular.module('UserController', ['matrixService']) avatar_url: undefined }; + $scope.user_id = matrixService.config().user_id; + matrixService.getDisplayName($scope.user.id).then( function(response) { $scope.user.displayname = response.data.displayname; } ); + matrixService.getProfilePictureUrl($scope.user.id).then( function(response) { $scope.user.avatar_url = response.data.avatar_url; } ); + + $scope.messageUser = function() { + + // FIXME: create a new room every time, for now + + matrixService.create(null, 'private').then( + function(response) { + // This room has been created. Refresh the rooms list + var room_id = response.data.room_id; + console.log("Created room with id: "+ room_id); + + matrixService.invite(room_id, $scope.user.id).then( + function() { + $scope.feedback = "Invite sent successfully"; + $scope.$parent.goToPage("/room/" + room_id); + }, + function(reason) { + $scope.feedback = "Failure: " + JSON.stringify(reason); + }); + }, + function(error) { + $scope.feedback = "Failure: " + JSON.stringify(error.data); + }); + }; + }]); \ No newline at end of file diff --git a/webclient/user/user.html b/webclient/user/user.html index 4c91c8a48a..2aa981437b 100644 --- a/webclient/user/user.html +++ b/webclient/user/user.html @@ -1,31 +1,25 @@ <div ng-controller="UserController" class="user"> - <h1 id="logo">[matrix]</h1> - <div id="page"> <div id="wrapper"> - + + <div id="genericHeading"> + <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> + </div> + + <h1>{{ user.displayname || user.id }}</h1> + <div> - <form> - <table> - <tr> - <td> - <div class="profile-avatar"> - <img ng-src="{{ user.avatar_url || 'img/default-profile.jpg' }}"/> - </div> - </td> - <td> - <div id="user-ids"> - <div id="user-displayname">{{ user.displayname }}</div> - <div>{{ user.id }}</div> - </div> - </td> - </tr> - </table> - </form> + <div class="profile-avatar"> + <img ng-src="{{ user.avatar_url || 'img/default-profile.png' }}"/> + </div> + <div id="user-ids"> + <div>{{ user.id }}</div> + </div> </div> + <button ng-hide="user.id == user_id" ng-click="messageUser()" style="font-size: 14pt; margin-top: 40px; margin-bottom: 40px">Start chat</button> + <br/> {{ feedback }} - </div> </div> </div> |