From 7dc0a28e17bff5172c303497b6c1ca40af23e7e9 Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Thu, 14 Aug 2014 11:36:11 +0200 Subject: Created m-file-input. A directive to open a file selection dialog on whatever HTML element --- .../components/fileInput/file-input-directive.js | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 webclient/components/fileInput/file-input-directive.js (limited to 'webclient/components') diff --git a/webclient/components/fileInput/file-input-directive.js b/webclient/components/fileInput/file-input-directive.js new file mode 100644 index 0000000000..9b73f877e9 --- /dev/null +++ b/webclient/components/fileInput/file-input-directive.js @@ -0,0 +1,43 @@ +/* + 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. + */ + +'use strict'; + +/* + * Transform an element into an image file input button. + * Watch to the passed variable change. It will contain the selected HTML5 file object. + */ +angular.module('mFileInput', []) +.directive('mFileInput', function() { + return { + restrict: 'A', + transclude: 'true', + template: '
', + scope: { + selectedFile: '=mFileInput' + }, + + link: function(scope, element, attrs, ctrl) { + element.bind("click", function() { + element.find("input")[0].click(); + element.find("input").bind("change", function(e) { + scope.selectedFile = this.files[0]; + scope.$apply(); + }); + }); + } + }; +}); \ No newline at end of file -- cgit 1.5.1 From 7143f358f1487d4044cc5ad64056f621a5aa2139 Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Thu, 14 Aug 2014 14:59:33 +0200 Subject: Detect when the user access token is no more valid and log the user out in this case --- webclient/app-controller.js | 10 ++++++++-- webclient/components/matrix/matrix-service.js | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) (limited to 'webclient/components') diff --git a/webclient/app-controller.js b/webclient/app-controller.js index 41055bdcd2..086fa3d946 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -55,8 +55,14 @@ angular.module('MatrixWebClientController', ['matrixService']) // And go to the login page $location.path("login"); - }; - + }; + + // Listen to the event indicating that the access token is no more valid. + // In this case, the user needs to log in again. + $scope.$on("M_UNKNOWN_TOKEN", function() { + console.log("Invalid access token -> log user out"); + $scope.logout(); + }); }]); \ No newline at end of file diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index f054bf301e..81ccdc2cc0 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -17,7 +17,7 @@ limitations under the License. 'use strict'; angular.module('matrixService', []) -.factory('matrixService', ['$http', '$q', function($http, $q) { +.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) { /* * Permanent storage of user information @@ -60,7 +60,6 @@ angular.module('matrixService', []) 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) { @@ -70,6 +69,11 @@ angular.module('matrixService', []) reason = JSON.stringify(data); } deferred.reject(reason, data, status, headers, config); + + if (403 === status && "M_UNKNOWN_TOKEN" === data.errcode) { + // The access token is no more valid, broadcast the issue + $rootScope.$broadcast("M_UNKNOWN_TOKEN"); + } }); return deferred.promise; @@ -301,6 +305,12 @@ angular.module('matrixService', []) return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); }, + + // + testLogin: function() { + + }, + /****** Permanent storage of user information ******/ // Returns the current config -- cgit 1.5.1 From db3e1d73c6a81bda3b2624596ea9b3f113242d38 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Aug 2014 15:36:40 +0100 Subject: Move the unknown token broadcast to the interceptor. Return the $http promise and not a wrapped one via $q. Everything now needs a level deeper nesting. Fixed registration and login. --- webclient/app.js | 10 +++++----- webclient/components/matrix/matrix-service.js | 21 +-------------------- webclient/login/login-controller.js | 14 +++++++++----- webclient/login/login.html | 1 + 4 files changed, 16 insertions(+), 30 deletions(-) (limited to 'webclient/components') diff --git a/webclient/app.js b/webclient/app.js index f869309449..0b613fa206 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -42,20 +42,20 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', redirectTo: '/rooms' }); - $provide.factory('AccessTokenInterceptor', function ($q) { + $provide.factory('AccessTokenInterceptor', ['$q', '$rootScope', + function ($q, $rootScope) { return { responseError: function(rejection) { - console.log("Rejection: " + JSON.stringify(rejection)); if (rejection.status === 403 && "data" in rejection && "errcode" in rejection.data && rejection.data.errcode === "M_UNKNOWN_TOKEN") { - console.log("TODO: Got a 403 with an unknown token. Logging out.") - // TODO logout + console.log("Got a 403 with an unknown token. Logging out.") + $rootScope.$broadcast("M_UNKNOWN_TOKEN"); } return $q.reject(rejection); } }; - }); + }]); $httpProvider.interceptors.push('AccessTokenInterceptor'); }]); diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 81ccdc2cc0..132c996f7a 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -49,32 +49,13 @@ angular.module('matrixService', []) 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({ + return $http({ method: method, url: baseUrl + path, params: params, data: data, headers: headers }) - .success(function(data, status, headers, config) { - 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); - - if (403 === status && "M_UNKNOWN_TOKEN" === data.errcode) { - // The access token is no more valid, broadcast the issue - $rootScope.$broadcast("M_UNKNOWN_TOKEN"); - } - }); return deferred.promise; }; diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js index fa91bf4253..c519f7698c 100644 --- a/webclient/login/login-controller.js +++ b/webclient/login/login-controller.js @@ -39,14 +39,13 @@ angular.module('LoginController', ['matrixService']) } matrixService.register($scope.account.desired_user_name, $scope.account.pwd1).then( - function(data) { + function(response) { $scope.feedback = "Success"; - // Update the current config var config = matrixService.config(); angular.extend(config, { - access_token: data.access_token, - user_id: data.user_id + access_token: response.data.access_token, + user_id: response.data.user_id }); matrixService.setConfig(config); @@ -74,7 +73,7 @@ angular.module('LoginController', ['matrixService']) matrixService.setConfig({ homeserver: $scope.account.homeserver, user_id: $scope.account.user_id, - access_token: response.access_token + access_token: response.data.access_token }); matrixService.saveConfig(); $location.path("rooms"); @@ -82,6 +81,11 @@ angular.module('LoginController', ['matrixService']) else { $scope.feedback = "Failed to login: " + JSON.stringify(response); } + }, + function(error) { + if (error.data.errcode === "M_FORBIDDEN") { + $scope.login_error_msg = "Incorrect username or password."; + } } ); }; diff --git a/webclient/login/login.html b/webclient/login/login.html index 508ff5e4bf..f02dde89a6 100644 --- a/webclient/login/login.html +++ b/webclient/login/login.html @@ -22,6 +22,7 @@

Got an account?

+
{{ login_error_msg }}

-- cgit 1.5.1 From 30da8c81c761a1f58c9643f41450240bfe1d6cc5 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Aug 2014 17:23:47 +0100 Subject: webclient: You can now paginate in rooms. Defaults to 10 messages, with a button to get more (needs to be hooked into infini-scrolling). --- docs/client-server/specification.rst | 3 + webclient/components/matrix/matrix-service.js | 11 ++++ webclient/room/room-controller.js | 80 ++++++++++++++++++++------- webclient/room/room.html | 1 + 4 files changed, 74 insertions(+), 21 deletions(-) (limited to 'webclient/components') diff --git a/docs/client-server/specification.rst b/docs/client-server/specification.rst index 97c8587a6d..b82093f2d3 100644 --- a/docs/client-server/specification.rst +++ b/docs/client-server/specification.rst @@ -414,6 +414,9 @@ The server checks this, finds it is valid, and returns: { "access_token": "abcdef0123456789" } +The server may optionally return "user_id" to confirm or change the user's ID. +This is particularly useful if the home server wishes to support localpart entry +of usernames (e.g. "bob" rather than "@bob:matrix.org"). OAuth2-based ------------ diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 132c996f7a..6d66111469 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -212,6 +212,17 @@ angular.module('matrixService', []) path = path.replace("$room_id", room_id); return doRequest("GET", path); }, + + paginateBackMessages: function(room_id, from_token, limit) { + var path = "/rooms/$room_id/messages/list"; + path = path.replace("$room_id", room_id); + var params = { + from: from_token, + to: "START", + limit: limit + }; + return doRequest("GET", path, params); + }, // get a list of public rooms on your home server publicRooms: function() { diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index cec19f7994..8003105654 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -18,11 +18,14 @@ angular.module('RoomController', []) .controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', function($scope, $http, $timeout, $routeParams, $location, matrixService) { 'use strict'; + var MESSAGES_PER_PAGINATION = 10; $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" + events_from: "END", // when to start the event stream from. + earliest_token: "END", // stores how far back we've paginated. + can_paginate: true }; $scope.messages = []; $scope.members = {}; @@ -30,6 +33,53 @@ angular.module('RoomController', []) $scope.imageURLToSend = ""; $scope.userIDToInvite = ""; + + var scrollToBottom = function() { + $timeout(function() { + var objDiv = document.getElementsByClassName("messageTableWrapper")[0]; + objDiv.scrollTop = objDiv.scrollHeight; + },0); + }; + + var parseChunk = function(chunks, appendToStart) { + for (var i = 0; i < chunks.length; i++) { + var chunk = chunks[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; + } + if (appendToStart) { + $scope.messages.unshift(chunk); + } + else { + $scope.messages.push(chunk); + scrollToBottom(); + } + } + else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") { + updateMemberList(chunk); + } + else if (chunk.type === "m.presence") { + updatePresence(chunk); + } + } + }; + + var paginate = function(numItems) { + matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then( + function(response) { + parseChunk(response.data.chunk, true); + $scope.state.earliest_token = response.data.end; + if (response.data.chunk.length < MESSAGES_PER_PAGINATION) { + // no more messages to paginate :( + $scope.state.can_paginate = false; + } + }, + function(error) { + console.log("paginateBackMessages Ruh roh: " + JSON.stringify(error)); + } + ) + }; var shortPoll = function() { $http.get(matrixService.config().homeserver + matrixService.prefix + "/events", { @@ -41,28 +91,10 @@ angular.module('RoomController', []) .then(function(response) { console.log("Got response from "+$scope.state.events_from+" to "+response.data.end); $scope.state.events_from = response.data.end; - $scope.feedback = ""; - 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() { - var objDiv = document.getElementsByClassName("messageTableWrapper")[0]; - objDiv.scrollTop = objDiv.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); - } - } + parseChunk(response.data.chunk, false); + if ($scope.stopPoll) { console.log("Stopping polling."); } @@ -199,6 +231,8 @@ angular.module('RoomController', []) $scope.feedback = "Failed get member list: " + error.data.error; } ); + + paginate(MESSAGES_PER_PAGINATION); }, function(reason) { $scope.feedback = "Can't join room: " + reason; @@ -238,6 +272,10 @@ angular.module('RoomController', []) $scope.feedback = "Failed to send image: " + error.data.error; }); }; + + $scope.loadMoreHistory = function() { + paginate(MESSAGES_PER_PAGINATION); + }; $scope.$on('$destroy', function(e) { console.log("onDestroyed: Stopping poll."); diff --git a/webclient/room/room.html b/webclient/room/room.html index 91e900c678..0f86a158ec 100644 --- a/webclient/room/room.html +++ b/webclient/room/room.html @@ -86,6 +86,7 @@ +
-- cgit 1.5.1 From f5973d8ddb1d972221370022459e9c750c079ad8 Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Thu, 14 Aug 2014 18:38:42 +0200 Subject: Create a temporary upload service server side (by hacking demos/webserver.py) and client side with an angularjs service component. --- demo/webserver.py | 25 +++++++++++- .../components/fileUpload/file-upload-service.js | 47 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 webclient/components/fileUpload/file-upload-service.js (limited to 'webclient/components') diff --git a/demo/webserver.py b/demo/webserver.py index 78f3213540..875095c877 100644 --- a/demo/webserver.py +++ b/demo/webserver.py @@ -2,9 +2,32 @@ import argparse import BaseHTTPServer import os import SimpleHTTPServer +import cgi, logging from daemonize import Daemonize +class SimpleHTTPRequestHandlerWithPOST(SimpleHTTPServer.SimpleHTTPRequestHandler): + UPLOAD_PATH = "upload" + + """ + Accept all post request as file upload + """ + def do_POST(self): + + path = os.path.join(self.UPLOAD_PATH, os.path.basename(self.path)) + length = self.headers['content-length'] + data = self.rfile.read(int(length)) + + with open(path, 'wb') as fh: + fh.write(data) + + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + + # Return the absolute path of the uploaded file + self.wfile.write('{"url":"/%s"}' % path) + def setup(): parser = argparse.ArgumentParser() @@ -19,7 +42,7 @@ def setup(): httpd = BaseHTTPServer.HTTPServer( ('', args.port), - SimpleHTTPServer.SimpleHTTPRequestHandler + SimpleHTTPRequestHandlerWithPOST ) def run(): diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js new file mode 100644 index 0000000000..5729d5da48 --- /dev/null +++ b/webclient/components/fileUpload/file-upload-service.js @@ -0,0 +1,47 @@ +/* + 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. + */ + +'use strict'; + +/* + * Upload an HTML5 file to a server + */ +angular.module('mFileUpload', []) +.service('mFileUpload', ['$http', '$q', function ($http, $q) { + + /* + * Upload an HTML5 file to a server and returned a promise + * that will provide the URL of the uploaded file. + */ + this.uploadFile = function(file) { + var deferred = $q.defer(); + + // @TODO: This service runs with the do_POST hacky implementation of /synapse/demos/webserver.py. + // This is temporary until we have a true file upload service + console.log("Uploading " + file.name + "..."); + $http.post(file.name, file) + .success(function(data, status, headers, config) { + deferred.resolve(location.origin + data.url); + console.log(" -> Successfully uploaded! Available at " + location.origin + data.url); + }). + error(function(data, status, headers, config) { + console.log(" -> Failed to upload" + file.name); + deferred.reject(); + }); + + return deferred.promise; + }; +}]); \ No newline at end of file -- cgit 1.5.1