diff --git a/webclient/CAPTCHA_SETUP b/webclient/CAPTCHA_SETUP
new file mode 100644
index 0000000000..ebc8a5f3b0
--- /dev/null
+++ b/webclient/CAPTCHA_SETUP
@@ -0,0 +1,46 @@
+Captcha can be enabled for this web client / home server. This file explains how to do that.
+The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google.
+
+Getting keys
+------------
+Requires a public/private key pair from:
+
+https://developers.google.com/recaptcha/
+
+
+Setting Private ReCaptcha Key
+-----------------------------
+The private key is a config option on the home server config. If it is not
+visible, you can generate it via --generate-config. Set the following value:
+
+ recaptcha_private_key: YOUR_PRIVATE_KEY
+
+In addition, you MUST enable captchas via:
+
+ enable_registration_captcha: true
+
+Setting Public ReCaptcha Key
+----------------------------
+The web client will look for the global variable webClientConfig for config
+options. You should put your ReCaptcha public key there like so:
+
+webClientConfig = {
+ useCaptcha: true,
+ recaptcha_public_key: "YOUR_PUBLIC_KEY"
+}
+
+This should be put in webclient/config.js which is already .gitignored, rather
+than in the web client source files. You MUST set useCaptcha to true else a
+ReCaptcha widget will not be generated.
+
+Configuring IP used for auth
+----------------------------
+The ReCaptcha API requires that the IP address of the user who solved the
+captcha is sent. If the client is connecting through a proxy or load balancer,
+it may be required to use the X-Forwarded-For (XFF) header instead of the origin
+IP address. This can be configured as an option on the home server like so:
+
+ captcha_ip_origin_is_x_forwarded: true
+
+
+
diff --git a/webclient/README b/webclient/README
index 0f893b1712..13224c3d07 100644
--- a/webclient/README
+++ b/webclient/README
@@ -1,12 +1,13 @@
Basic Usage
-----------
-The Synapse web client needs to be hosted by a basic HTTP server.
-
-You can use the Python simple HTTP server::
+The web client should automatically run when running the home server. Alternatively, you can run
+it stand-alone:
$ python -m SimpleHTTPServer
Then, open this URL in a WEB browser::
http://127.0.0.1:8000/
+
+
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index ea48cbb011..064bde3ab2 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', 'matrixPhoneService',
- function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService, matrixPhoneService) {
+.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService',
+ function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, matrixPhoneService) {
// Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path();
@@ -89,6 +89,23 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
$scope.user_id = matrixService.config().user_id;
};
+ $rootScope.$watch('currentCall', function(newVal, oldVal) {
+ if (!$rootScope.currentCall) return;
+
+ var roomMembers = angular.copy($rootScope.events.rooms[$rootScope.currentCall.room_id].members);
+ delete roomMembers[matrixService.config().user_id];
+
+ $rootScope.currentCall.user_id = Object.keys(roomMembers)[0];
+ matrixService.getProfile($rootScope.currentCall.user_id).then(
+ function(response) {
+ $rootScope.currentCall.userProfile = response.data;
+ },
+ function(error) {
+ $scope.feedback = "Can't load user profile";
+ }
+ );
+ });
+
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
console.trace("incoming call");
call.onError = $scope.onCallError;
@@ -97,12 +114,19 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
});
$scope.answerCall = function() {
- $scope.currentCall.answer();
+ $rootScope.currentCall.answer();
};
$scope.hangupCall = function() {
- $scope.currentCall.hangup();
- $scope.currentCall = undefined;
+ $rootScope.currentCall.hangup();
+
+ $timeout(function() {
+ var icon = angular.element('#callEndedIcon');
+ $animate.addClass(icon, 'callIconRotate');
+ $timeout(function(){
+ $rootScope.currentCall = undefined;
+ }, 2000);
+ }, 100);
};
$rootScope.onCallError = function(errStr) {
@@ -110,5 +134,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
}
$rootScope.onCallHangup = function() {
+ $timeout(function() {
+ var icon = angular.element('#callEndedIcon');
+ $animate.addClass(icon, 'callIconRotate');
+ $timeout(function(){
+ $rootScope.currentCall = undefined;
+ }, 2000);
+ }, 100);
}
}]);
diff --git a/webclient/app-filter.js b/webclient/app-filter.js
index 27f435674f..ee9374668b 100644
--- a/webclient/app-filter.js
+++ b/webclient/app-filter.js
@@ -79,85 +79,4 @@ 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) {
- var room_name_event = room["m.room.name"];
-
- if (room_name_event) {
- roomName = room_name_event.content.name;
- }
- else 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.state_key !== matrixService.config().user_id) {
-
- if (member.state_key in $rootScope.presence) {
- // If the user is available in presence, use the displayname there
- // as it is the most uptodate
- roomName = $rootScope.presence[member.state_key].content.displayname;
- }
- else if (member.content.displayname) {
- roomName = member.content.displayname;
- }
- else {
- roomName = member.state_key;
- }
- }
- }
- }
- else if (1 === Object.keys(room.members).length) {
- // The other member may be in the invite list, get all invited users
- var invitedUserIDs = [];
- for (var i in room.messages) {
- var message = room.messages[i];
- if ("m.room.member" === message.type && "invite" === message.membership) {
- // Make sure there is no duplicate user
- if (-1 === invitedUserIDs.indexOf(message.state_key)) {
- invitedUserIDs.push(message.state_key);
- }
- }
- }
-
- // For now, only 1:1 room needs to be renamed. It means only 1 invited user
- if (1 === invitedUserIDs.length) {
- var userID = invitedUserIDs[0];
-
- // Try to resolve his displayname in presence global data
- if (userID in $rootScope.presence) {
- roomName = $rootScope.presence[userID].content.displayname;
- }
- else {
- roomName = userID;
- }
- }
- }
- }
- }
- }
-
- if (undefined === roomName) {
- // By default, use the room ID
- roomName = room_id;
- }
-
- return roomName;
- };
-}]);
+}]);
\ No newline at end of file
diff --git a/webclient/app.css b/webclient/app.css
index 425d5bb11a..7698cb4fda 100755
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -44,7 +44,49 @@ a:active { color: #000; }
}
#callBar {
- float: left;
+ float: left;
+ height: 32px;
+ margin: auto;
+ text-align: right;
+ line-height: 16px;
+}
+
+.callIcon {
+ margin-left: 4px;
+ margin-right: 4px;
+ margin-top: 8px;
+ -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+ -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+ -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+ transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+}
+
+.callIconRotate {
+ -webkit-transform: rotateZ(45deg);
+ -moz-transform: rotateZ(45deg);
+ -ms-transform: rotateZ(45deg);
+ -o-transform: rotateZ(45deg);
+ transform: rotateZ(45deg);
+}
+
+#callPeerImage {
+ width: 32px;
+ height: 32px;
+ border: none;
+ float: left;
+}
+
+#callPeerNameAndState {
+ float: left;
+ margin-left: 4px;
+}
+
+#callState {
+ font-size: 60%;
+}
+
+#callPeerName {
+ font-size: 80%;
}
#headerContent {
@@ -105,6 +147,10 @@ a:active { color: #000; }
text-align: center;
}
+#recaptcha_area {
+ margin: auto
+}
+
#loginForm {
text-align: left;
padding: 1em;
@@ -251,12 +297,14 @@ a:active { color: #000; }
.userAvatar .userAvatarImage {
position: absolute;
top: 0px;
- object-fit: cover;
+ object-fit: cover;
+ width: 100%;
}
.userAvatar .userAvatarGradient {
position: absolute;
bottom: 20px;
+ width: 100%;
}
.userAvatar .userName {
@@ -417,6 +465,13 @@ a:active { color: #000; }
text-align: left ! important;
}
+.bubble .messagePending {
+ opacity: 0.3
+}
+.messageUnSent {
+ color: #F00;
+}
+
#room-fullscreen-image {
position: absolute;
top: 0px;
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index ee478d2eb0..d2bb31053f 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -41,6 +41,11 @@ angular.module('eventHandlerService', [])
$rootScope.events = {
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
};
+
+ // used for dedupping events - could be expanded in future...
+ // FIXME: means that we leak memory over time (along with lots of the rest
+ // of the app, given we never try to reap memory yet)
+ var eventMap = {};
$rootScope.presence = {};
@@ -66,11 +71,22 @@ angular.module('eventHandlerService', [])
$rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
};
+ var handleRoomAliases = function(event, isLiveEvent) {
+ matrixService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
+ };
+
var handleMessage = function(event, isLiveEvent) {
initRoom(event.room_id);
if (isLiveEvent) {
- $rootScope.events.rooms[event.room_id].messages.push(event);
+ if (event.user_id === matrixService.config().user_id &&
+ (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) {
+ // assume we've already echoed it
+ // FIXME: track events by ID and ungrey the right message to show it's been delivered
+ }
+ else {
+ $rootScope.events.rooms[event.room_id].messages.push(event);
+ }
}
else {
$rootScope.events.rooms[event.room_id].messages.unshift(event);
@@ -87,6 +103,14 @@ angular.module('eventHandlerService', [])
var handleRoomMember = function(event, isLiveEvent) {
initRoom(event.room_id);
+ // if the server is stupidly re-relaying a no-op join, discard it.
+ if (event.prev_content &&
+ event.content.membership === "join" &&
+ event.content.membership === event.prev_content.membership)
+ {
+ return;
+ }
+
// add membership changes as if they were a room message if something interesting changed
if (event.content.prev !== event.content.membership) {
if (isLiveEvent) {
@@ -137,40 +161,55 @@ angular.module('eventHandlerService', [])
POWERLEVEL_EVENT: POWERLEVEL_EVENT,
CALL_EVENT: CALL_EVENT,
NAME_EVENT: NAME_EVENT,
-
handleEvent: function(event, isLiveEvent) {
- switch(event.type) {
- case "m.room.create":
- handleRoomCreate(event, isLiveEvent);
- break;
- case "m.room.message":
- handleMessage(event, isLiveEvent);
- break;
- case "m.room.member":
- handleRoomMember(event, isLiveEvent);
- break;
- case "m.presence":
- handlePresence(event, isLiveEvent);
- break;
- case 'm.room.ops_levels':
- case 'm.room.send_event_level':
- case 'm.room.add_state_level':
- case 'm.room.join_rules':
- case 'm.room.power_levels':
- handlePowerLevels(event, isLiveEvent);
- break;
- case 'm.room.name':
- handleRoomName(event, isLiveEvent);
- break;
- default:
- console.log("Unable to handle event type " + event.type);
- console.log(JSON.stringify(event, undefined, 4));
- break;
+ // FIXME: event duplication suppression is all broken as the code currently expect to handles
+ // events multiple times to get their side-effects...
+/*
+ if (eventMap[event.event_id]) {
+ console.log("discarding duplicate event: " + JSON.stringify(event));
+ return;
}
+ else {
+ eventMap[event.event_id] = 1;
+ }
+*/
if (event.type.indexOf('m.call.') === 0) {
handleCallEvent(event, isLiveEvent);
}
+ else {
+ switch(event.type) {
+ case "m.room.create":
+ handleRoomCreate(event, isLiveEvent);
+ break;
+ case "m.room.aliases":
+ handleRoomAliases(event, isLiveEvent);
+ break;
+ case "m.room.message":
+ handleMessage(event, isLiveEvent);
+ break;
+ case "m.room.member":
+ handleRoomMember(event, isLiveEvent);
+ break;
+ case "m.presence":
+ handlePresence(event, isLiveEvent);
+ break;
+ case 'm.room.ops_levels':
+ case 'm.room.send_event_level':
+ case 'm.room.add_state_level':
+ case 'm.room.join_rules':
+ case 'm.room.power_levels':
+ handlePowerLevels(event, isLiveEvent);
+ break;
+ case 'm.room.name':
+ handleRoomName(event, isLiveEvent);
+ break;
+ default:
+ console.log("Unable to handle event type " + event.type);
+ console.log(JSON.stringify(event, undefined, 4));
+ break;
+ }
+ }
},
// isLiveEvents determines whether notifications should be shown, whether
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 1c0f7712b4..ed4f3b2ffc 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -110,6 +110,7 @@ angular.module('eventStreamService', [])
var rooms = response.data.rooms;
for (var i = 0; i < rooms.length; ++i) {
var room = rooms[i];
+ // console.log("got room: " + room.room_id);
if ("state" in room) {
eventHandlerService.handleEvents(room.state, false);
}
diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js
index 3e13e4e81f..3cb5e8b693 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/webclient/components/matrix/matrix-call.js
@@ -41,6 +41,7 @@ angular.module('MatrixCall', [])
this.room_id = room_id;
this.call_id = "c" + new Date().getTime();
this.state = 'fledgling';
+ this.didConnect = false;
}
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
@@ -52,6 +53,7 @@ angular.module('MatrixCall', [])
matrixPhoneService.callPlaced(this);
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); });
self.state = 'wait_local_media';
+ this.direction = 'outbound';
};
MatrixCall.prototype.initWithInvite = function(msg) {
@@ -64,6 +66,7 @@ angular.module('MatrixCall', [])
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
this.state = 'ringing';
+ this.direction = 'inbound';
};
MatrixCall.prototype.answer = function() {
@@ -204,10 +207,12 @@ angular.module('MatrixCall', [])
};
MatrixCall.prototype.onIceConnectionStateChanged = function() {
+ if (this.state == 'ended') return; // because ICE can still complete as we're ending the call
console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState);
// ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
this.state = 'connected';
+ this.didConnect = true;
$rootScope.$apply();
}
};
diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js
new file mode 100644
index 0000000000..260e0827df
--- /dev/null
+++ b/webclient/components/matrix/matrix-filter.js
@@ -0,0 +1,135 @@
+/*
+ Copyright 2014 OpenMarket Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+'use strict';
+
+angular.module('matrixFilter', [])
+
+// Compute the room name according to information we have
+.filter('mRoomName', ['$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) {
+ var room_name_event = room["m.room.name"];
+
+ if (room_name_event) {
+ roomName = room_name_event.content.name;
+ }
+ else 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.state_key !== matrixService.config().user_id) {
+
+ if (member.state_key in $rootScope.presence) {
+ // If the user is available in presence, use the displayname there
+ // as it is the most uptodate
+ roomName = $rootScope.presence[member.state_key].content.displayname;
+ }
+ else if (member.content.displayname) {
+ roomName = member.content.displayname;
+ }
+ else {
+ roomName = member.state_key;
+ }
+ }
+ }
+ }
+ else if (1 === Object.keys(room.members).length) {
+ // The other member may be in the invite list, get all invited users
+ var invitedUserIDs = [];
+ for (var i in room.messages) {
+ var message = room.messages[i];
+ if ("m.room.member" === message.type && "invite" === message.membership) {
+ // Make sure there is no duplicate user
+ if (-1 === invitedUserIDs.indexOf(message.state_key)) {
+ invitedUserIDs.push(message.state_key);
+ }
+ }
+ }
+
+ // For now, only 1:1 room needs to be renamed. It means only 1 invited user
+ if (1 === invitedUserIDs.length) {
+ var userID = invitedUserIDs[0];
+
+ // Try to resolve his displayname in presence global data
+ if (userID in $rootScope.presence) {
+ roomName = $rootScope.presence[userID].content.displayname;
+ }
+ else {
+ roomName = userID;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (undefined === roomName) {
+ // By default, use the room ID
+ roomName = room_id;
+ }
+
+ return roomName;
+ };
+}])
+
+// Compute the user display name in a room according to the data already downloaded
+.filter('mUserDisplayName', ['$rootScope', function($rootScope) {
+ return function(user_id, room_id) {
+ var displayName;
+
+ // Try to find the user name among presence data
+ // Warning: that means we have received before a presence event for this
+ // user which cannot be guaranted.
+ // However, if we get the info by this way, we are sure this is the latest user display name
+ // See FIXME comment below
+ if (user_id in $rootScope.presence) {
+ displayName = $rootScope.presence[user_id].content.displayname;
+ }
+
+ // FIXME: Would like to use the display name as defined in room members of the room.
+ // But this information is the display name of the user when he has joined the room.
+ // It does not take into account user display name update
+ if (room_id) {
+ var room = $rootScope.events.rooms[room_id];
+ if (room && (user_id in room.members)) {
+ var member = room.members[user_id];
+ if (member.content.displayname) {
+ displayName = member.content.displayname;
+ }
+ }
+ }
+
+ if (undefined === displayName) {
+ // By default, use the user ID
+ displayName = user_id;
+ }
+ return displayName;
+ };
+}]);
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index 25222a9e9e..3c28c52fbe 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -36,6 +36,9 @@ angular.module('matrixService', [])
*/
var config;
+ var roomIdToAlias = {};
+ var aliasToRoomId = {};
+
// Current version of permanent storage
var configVersion = 0;
var prefixPath = "/_matrix/client/api/v1";
@@ -84,15 +87,32 @@ angular.module('matrixService', [])
prefix: prefixPath,
// Register an user
- register: function(user_name, password, threepidCreds) {
+ register: function(user_name, password, threepidCreds, useCaptcha) {
// The REST path spec
var path = "/register";
-
- return doRequest("POST", path, undefined, {
+
+ var data = {
user_id: user_name,
password: password,
threepidCreds: threepidCreds
- });
+ };
+
+ if (useCaptcha) {
+ // Not all home servers will require captcha on signup, but if this flag is checked,
+ // send captcha information.
+ // TODO: Might be nice to make this a bit more flexible..
+ var challengeToken = Recaptcha.get_challenge();
+ var captchaEntry = Recaptcha.get_response();
+ var captchaType = "m.login.recaptcha";
+
+ data.captcha = {
+ type: captchaType,
+ challenge: challengeToken,
+ response: captchaEntry
+ };
+ }
+
+ return doRequest("POST", path, undefined, data);
},
// Create a room
@@ -168,18 +188,20 @@ angular.module('matrixService', [])
},
// Change the membership of an another user
- setMembership: function(room_id, user_id, membershipValue) {
+ setMembership: function(room_id, user_id, membershipValue, reason) {
+
// The REST path spec
var path = "/rooms/$room_id/state/m.room.member/$user_id";
path = path.replace("$room_id", encodeURIComponent(room_id));
path = path.replace("$user_id", user_id);
return doRequest("PUT", path, undefined, {
- membership: membershipValue
+ membership : membershipValue,
+ reason: reason
});
},
- // Bans a user from from a room
+ // Bans a user from a room
ban: function(room_id, user_id, reason) {
var path = "/rooms/$room_id/ban";
path = path.replace("$room_id", encodeURIComponent(room_id));
@@ -189,7 +211,20 @@ angular.module('matrixService', [])
reason: reason
});
},
-
+
+ // Unbans a user in a room
+ unban: function(room_id, user_id) {
+ // FIXME: To update when there will be homeserver API for unban
+ // For now, do an unban by resetting the user membership to "leave"
+ return this.setMembership(room_id, user_id, "leave");
+ },
+
+ // Kicks a user from a room
+ kick: function(room_id, user_id, reason) {
+ // Set the user membership to "leave" to kick him
+ return this.setMembership(room_id, user_id, "leave", reason);
+ },
+
// Retrieves the room ID corresponding to a room alias
resolveRoomAlias:function(room_alias) {
var path = "/_matrix/client/api/v1/directory/room/$room_alias";
@@ -280,6 +315,11 @@ angular.module('matrixService', [])
return doRequest("GET", path);
},
+ // get a user's profile
+ getProfile: function(userId) {
+ return this.getProfileInfo(userId);
+ },
+
// get a display name for this user ID
getDisplayName: function(userId) {
return this.getProfileInfo(userId, "displayname");
@@ -313,8 +353,8 @@ angular.module('matrixService', [])
},
getProfileInfo: function(userId, info_segment) {
- var path = "/profile/$user_id/" + info_segment;
- path = path.replace("$user_id", userId);
+ var path = "/profile/"+userId
+ if (info_segment) path += '/' + info_segment;
return doRequest("GET", path);
},
@@ -485,18 +525,20 @@ angular.module('matrixService', [])
room_alias: undefined,
room_display_name: undefined
};
-
var alias = this.getRoomIdToAliasMapping(room.room_id);
if (alias) {
// use the existing alias from storage
result.room_alias = alias;
result.room_display_name = alias;
}
+ // XXX: this only lets us learn aliases from our local HS - we should
+ // make the client stop returning this if we can trust m.room.aliases state events
else if (room.aliases && room.aliases[0]) {
// save the mapping
// TODO: select the smarter alias from the array
this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]);
result.room_display_name = room.aliases[0];
+ result.room_alias = room.aliases[0];
}
else if (room.membership === "invite" && "inviter" in room) {
result.room_display_name = room.inviter + "'s room";
@@ -509,13 +551,22 @@ angular.module('matrixService', [])
},
createRoomIdToAliasMapping: function(roomId, alias) {
- localStorage.setItem(MAPPING_PREFIX+roomId, alias);
+ roomIdToAlias[roomId] = alias;
+ aliasToRoomId[alias] = roomId;
+ // localStorage.setItem(MAPPING_PREFIX+roomId, alias);
},
getRoomIdToAliasMapping: function(roomId) {
- return localStorage.getItem(MAPPING_PREFIX+roomId);
+ var alias = roomIdToAlias[roomId]; // was localStorage.getItem(MAPPING_PREFIX+roomId)
+ //console.log("looking for alias for " + roomId + "; found: " + alias);
+ return alias;
},
+ getAliasToRoomIdMapping: function(alias) {
+ var roomId = aliasToRoomId[alias];
+ //console.log("looking for roomId for " + alias + "; found: " + roomId);
+ return roomId;
+ },
/****** Power levels management ******/
diff --git a/webclient/home/home.html b/webclient/home/home.html
index c1f9643839..7240e79f86 100644
--- a/webclient/home/home.html
+++ b/webclient/home/home.html
@@ -26,7 +26,7 @@
<div class="public_rooms" ng-repeat="room in public_rooms">
<div>
- <a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
+ <a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_display_name }}</a>
</div>
</div>
<br/>
diff --git a/webclient/img/green_phone.png b/webclient/img/green_phone.png
new file mode 100644
index 0000000000..28807c749b
--- /dev/null
+++ b/webclient/img/green_phone.png
Binary files differdiff --git a/webclient/img/red_phone.png b/webclient/img/red_phone.png
new file mode 100644
index 0000000000..11fc44940c
--- /dev/null
+++ b/webclient/img/red_phone.png
Binary files differdiff --git a/webclient/index.html b/webclient/index.html
index f016dbb877..81c7c7d06c 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -10,12 +10,14 @@
<meta name="viewport" content="width=device-width">
- <script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
+ <script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
+ <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
<script src="js/angular.min.js"></script>
<script src="js/angular-route.min.js"></script>
<script src="js/angular-sanitize.min.js"></script>
<script type='text/javascript' src='js/ng-infinite-scroll-matrix.js'></script>
<script src="app.js"></script>
+ <script src="config.js"></script>
<script src="app-controller.js"></script>
<script src="app-directive.js"></script>
<script src="app-filter.js"></script>
@@ -29,6 +31,7 @@
<script src="settings/settings-controller.js"></script>
<script src="user/user-controller.js"></script>
<script src="components/matrix/matrix-service.js"></script>
+ <script src="components/matrix/matrix-filter.js"></script>
<script src="components/matrix/matrix-call.js"></script>
<script src="components/matrix/matrix-phone-service.js"></script>
<script src="components/matrix/event-stream-service.js"></script>
@@ -44,18 +47,29 @@
<div id="header">
<!-- Do not show buttons on the login page -->
<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 id="callBar" ng-show="currentCall">
+ <img id="callPeerImage" ng-show="currentCall.userProfile.avatar_url" ngSrc="{{ currentCall.userProfile.avatar_url }}" />
+ <img class="callIcon" src="img/green_phone.png" ng-show="currentCall.state != 'ended'" />
+ <img class="callIcon" id="callEndedIcon" src="img/red_phone.png" ng-show="currentCall.state == 'ended'" />
+ <div id="callPeerNameAndState">
+ <span id="callPeerName">{{ currentCall.userProfile.displayname }}</span>
+ <br />
+ <span id="callState">
+ <span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
+ <span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
+ <span ng-show="currentCall.state == 'connected'">Call Connected</span>
+ <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound'">Call Rejected</span>
+ <span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'outbound'">Call Ended</span>
+ <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'inbound'">Call Canceled</span>
+ <span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'inbound'">Call Ended</span>
+ <span ng-show="currentCall.state == 'wait_local_media'">Waiting for media permission...</span>
+ </span>
</div>
+ <span ng-show="currentCall.state == 'ringing'">
+ <button ng-click="answerCall()">Answer</button>
+ <button ng-click="hangupCall()">Reject</button>
+ </span>
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
- <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>
diff --git a/webclient/login/login.html b/webclient/login/login.html
index 18e7a02815..6297ec4d42 100644
--- a/webclient/login/login.html
+++ b/webclient/login/login.html
@@ -39,8 +39,8 @@
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>
+ <a href="#/register" style="padding-right: 0em">Create account</a>
+ <a href="#/reset_password" style="display: none; ">Forgotten password?</a>
</div>
</div>
</form>
diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js
index 5a14964248..b3c0c21335 100644
--- a/webclient/login/register-controller.js
+++ b/webclient/login/register-controller.js
@@ -19,6 +19,12 @@ angular.module('RegisterController', ['matrixService'])
function($scope, $rootScope, $location, matrixService, eventStreamService) {
'use strict';
+ var config = window.webClientConfig;
+ var useCaptcha = true;
+ if (config !== undefined) {
+ useCaptcha = config.useCaptcha;
+ }
+
// FIXME: factor out duplication with login-controller.js
// Assume that this is hosted on the home server, in which case the URL
@@ -87,9 +93,12 @@ angular.module('RegisterController', ['matrixService'])
};
$scope.registerWithMxidAndPassword = function(mxid, password, threepidCreds) {
- matrixService.register(mxid, password, threepidCreds).then(
+ matrixService.register(mxid, password, threepidCreds, useCaptcha).then(
function(response) {
$scope.feedback = "Success";
+ if (useCaptcha) {
+ Recaptcha.destroy();
+ }
// Update the current config
var config = matrixService.config();
angular.extend(config, {
@@ -116,11 +125,21 @@ angular.module('RegisterController', ['matrixService'])
},
function(error) {
console.trace("Registration error: "+error);
+ if (useCaptcha) {
+ Recaptcha.reload();
+ }
if (error.data) {
if (error.data.errcode === "M_USER_IN_USE") {
$scope.feedback = "Username already taken.";
$scope.reenter_username = true;
}
+ else if (error.data.errcode == "M_CAPTCHA_INVALID") {
+ $scope.feedback = "Failed captcha.";
+ }
+ else if (error.data.errcode == "M_CAPTCHA_NEEDED") {
+ $scope.feedback = "Captcha is required on this home " +
+ "server.";
+ }
}
else if (error.status === 0) {
$scope.feedback = "Unable to talk to the server.";
@@ -142,6 +161,33 @@ angular.module('RegisterController', ['matrixService'])
}
);
};
+
+ var setupCaptcha = function() {
+ console.log("Setting up ReCaptcha")
+ var config = window.webClientConfig;
+ var public_key = undefined;
+ if (config === undefined) {
+ console.error("Couldn't find webClientConfig. Cannot get public key for captcha.");
+ }
+ else {
+ public_key = webClientConfig.recaptcha_public_key;
+ if (public_key === undefined) {
+ console.error("No public key defined for captcha!")
+ }
+ }
+ Recaptcha.create(public_key,
+ "regcaptcha",
+ {
+ theme: "red",
+ callback: Recaptcha.focus_response_field
+ });
+ };
+ $scope.init = function() {
+ if (useCaptcha) {
+ setupCaptcha();
+ }
+ };
+
}]);
diff --git a/webclient/login/register.html b/webclient/login/register.html
index 06a6526b70..a27f9ad4e8 100644
--- a/webclient/login/register.html
+++ b/webclient/login/register.html
@@ -12,7 +12,6 @@
<div style="text-align: center">
<br/>
-
<input ng-show="!wait_3pid_code" id="email" size="32" type="text" ng-focus="true" ng-model="account.email" placeholder="Email address (optional)"/>
<div ng-show="!wait_3pid_code" class="smallPrint">Specifying an email address lets other users find you on Matrix more easily,<br/>
and will give you a way to reset your password in the future</div>
@@ -26,7 +25,10 @@
<input ng-show="!wait_3pid_code" id="displayName" size="32" type="text" ng-model="account.displayName" placeholder="Display name (e.g. Bob Obson)"/>
<br ng-show="!wait_3pid_code" />
<br ng-show="!wait_3pid_code" />
-
+
+
+ <div id="regcaptcha" ng-init="init()" />
+
<button ng-show="!wait_3pid_code" ng-click="register()" ng-disabled="!account.desired_user_id || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Sign up</button>
<div ng-show="wait_3pid_code">
diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js
index 3209f2cbdf..0f27f7a660 100644
--- a/webclient/recents/recents-controller.js
+++ b/webclient/recents/recents-controller.js
@@ -16,7 +16,7 @@
'use strict';
-angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
+angular.module('RecentsController', ['matrixService', 'matrixFilter', 'eventHandlerService'])
.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService',
function($scope, matrixService, eventHandlerService) {
$scope.rooms = {};
@@ -28,13 +28,8 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
var listenToEventStream = function() {
// Refresh the list on matrix invitation and message event
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
- var config = matrixService.config();
- if (isLive && event.state_key === config.user_id && event.content.membership === "invite") {
- console.log("Invited to room " + event.room_id);
- // FIXME push membership to top level key to match /im/sync
- event.membership = event.content.membership;
-
- $scope.rooms[event.room_id] = event;
+ if (isLive) {
+ $scope.rooms[event.room_id].lastMsg = event;
}
});
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index 9978e08b13..280d0632ab 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_id | roomName }}
+ {{ room.room_id | mRoomName }}
</td>
<td class="recentsRoomSummaryTS">
{{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
@@ -16,27 +16,48 @@
<tr>
<td colspan="2" class="recentsRoomSummary">
- <div ng-show="room.membership === 'invite'" >
- {{ room.inviter }} invited you
+ <div ng-show="room.membership === 'invite'">
+ {{ room.lastMsg.inviter | mUserDisplayName: room.room_id }} invited you
</div>
-
- <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" >
- <div ng-switch-when="m.room.member">
- {{ room.lastMsg.user_id }}
- {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }}
- {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }}
+
+ <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type">
+ <div ng-switch-when="m.room.member">
+ <span ng-if="'join' === room.lastMsg.content.membership">
+ {{ room.lastMsg.state_key | mUserDisplayName: room.room_id}} joined
+ </span>
+ <span ng-if="'leave' === room.lastMsg.content.membership">
+ <span ng-if="room.lastMsg.user_id === room.lastMsg.state_key">
+ {{room.lastMsg.state_key | mUserDisplayName: room.room_id }} left
+ </span>
+ <span ng-if="room.lastMsg.user_id !== room.lastMsg.state_key">
+ {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
+ {{ {"join": "kicked", "ban": "unbanned"}[room.lastMsg.content.prev] }}
+ {{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
+ </span>
+ <span ng-if="'join' === room.lastMsg.content.prev && room.lastMsg.content.reason">
+ : {{ room.lastMsg.content.reason }}
+ </span>
+ </span>
+ <span ng-if="'invite' === room.lastMsg.content.membership || 'ban' === room.lastMsg.content.membership">
+ {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
+ {{ {"invite": "invited", "ban": "banned"}[room.lastMsg.content.membership] }}
+ {{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
+ <span ng-if="'ban' === room.lastMsg.content.prev && room.lastMsg.content.reason">
+ : {{ room.lastMsg.content.reason }}
+ </span>
+ </span>
</div>
<div ng-switch-when="m.room.message">
<div ng-switch="room.lastMsg.content.msgtype">
<div ng-switch-when="m.text">
- {{ room.lastMsg.user_id }} :
+ {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} :
<span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'">
</span>
</div>
<div ng-switch-when="m.image">
- {{ room.lastMsg.user_id }} sent an image
+ {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} sent an image
</div>
<div ng-switch-when="m.emote">
@@ -51,7 +72,7 @@
</div>
<div ng-switch-default>
- <div ng-if="room.lastMsg.type.indexOf('m.call.') == 0">
+ <div ng-if="room.lastMsg.type.indexOf('m.call.') === 0">
Call
</div>
</div>
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index c3f72c9d25..e69adb9b46 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-angular.module('RoomController', ['ngSanitize', 'mFileInput'])
+angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall',
function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) {
'use strict';
@@ -32,9 +32,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
first_pagination: true, // this is toggled off when the first pagination is done
can_paginate: true, // this is toggled off when we run out of items
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
- stream_failure: undefined, // the response when the stream fails
- // FIXME: sending has been disabled, as surely messages should be sent in the background rather than locking the UI synchronously --Matthew
- sending: false // true when a message is being sent. It helps to disable the UI when a process is running
+ stream_failure: undefined // the response when the stream fails
};
$scope.members = {};
$scope.autoCompleting = false;
@@ -44,18 +42,25 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
$scope.imageURLToSend = "";
$scope.userIDToInvite = "";
- var scrollToBottom = function() {
+ var scrollToBottom = function(force) {
console.log("Scrolling to bottom");
- $timeout(function() {
- var objDiv = document.getElementById("messageTableWrapper");
- objDiv.scrollTop = objDiv.scrollHeight;
- }, 0);
+
+ // Do not autoscroll to the bottom to display the new event if the user is not at the bottom.
+ // Exception: in case where the event is from the user, we want to force scroll to the bottom
+ var objDiv = document.getElementById("messageTableWrapper");
+ if ((objDiv.offsetHeight + objDiv.scrollTop >= objDiv.scrollHeight) || force) {
+
+ $timeout(function() {
+ objDiv.scrollTop = objDiv.scrollHeight;
+ }, 0);
+ }
};
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
if (isLive && event.room_id === $scope.room_id) {
- scrollToBottom();
+ scrollToBottom();
+
if (window.Notification) {
// Show notification when the user is idle
if (matrixService.presence.offline === mPresence.getState()) {
@@ -76,6 +81,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
if (isLive) {
+ scrollToBottom();
updateMemberList(event);
}
});
@@ -169,16 +175,18 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
var updateMemberList = function(chunk) {
if (chunk.room_id != $scope.room_id) return;
- // Ignore banned and kicked (leave) people
- if ("ban" === chunk.membership || "leave" === chunk.membership) {
- return;
- }
// set target_user_id to keep things clear
var target_user_id = chunk.state_key;
var isNewMember = !(target_user_id in $scope.members);
if (isNewMember) {
+
+ // Ignore banned and kicked (leave) people
+ if ("ban" === chunk.membership || "leave" === chunk.membership) {
+ return;
+ }
+
// FIXME: why are we copying these fields around inside chunk?
if ("presence" in chunk.content) {
chunk.presence = chunk.content.presence;
@@ -202,6 +210,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
}
else {
// selectively update membership and presence else it will nuke the picture and displayname too :/
+
+ // Remove banned and kicked (leave) people
+ if ("ban" === chunk.membership || "leave" === chunk.membership) {
+ delete $scope.members[target_user_id];
+ return;
+ }
+
var member = $scope.members[target_user_id];
member.membership = chunk.content.membership;
if ("presence" in chunk.content) {
@@ -256,7 +271,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
normaliseMembersPowerLevels();
}
- }
+ };
// Normalise users power levels so that the user with the higher power level
// will have a bar covering 100% of the width of his avatar
@@ -277,104 +292,225 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel;
}
}
- }
+ };
$scope.send = function() {
if ($scope.textInput === "") {
return;
}
-
- $scope.state.sending = true;
+
+ scrollToBottom(true);
var promise;
+ var cmd;
+ var args;
+ var echo = false;
// Check for IRC style commands first
- if ($scope.textInput.indexOf("/") === 0) {
- var args = $scope.textInput.split(' ');
- var cmd = args[0];
+ var line = $scope.textInput;
+
+ // trim any trailing whitespace, as it can confuse the parser for IRC-style commands
+ line = line.replace(/\s+$/, "");
+
+ if (line[0] === "/" && line[1] !== "/") {
+ var bits = line.match(/^(\S+?)( +(.*))?$/);
+ cmd = bits[1];
+ args = bits[3];
+
+ console.log("cmd: " + cmd + ", args: " + args);
switch (cmd) {
case "/me":
- var emoteMsg = args.slice(1).join(' ');
- promise = matrixService.sendEmoteMessage($scope.room_id, emoteMsg);
+ promise = matrixService.sendEmoteMessage($scope.room_id, args);
+ echo = true;
break;
case "/nick":
// Change user display name
- if (2 === args.length) {
- promise = matrixService.setDisplayName(args[1]);
+ if (args) {
+ promise = matrixService.setDisplayName(args);
+ }
+ else {
+ $scope.feedback = "Usage: /nick <display_name>";
+ }
+ break;
+
+ case "/join":
+ // Join a room
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ var room_alias = matches[1];
+ if (room_alias.indexOf(':') == -1) {
+ // FIXME: actually track the :domain style name of our homeserver
+ // with or without port as is appropriate and append it at this point
+ }
+
+ var room_id = matrixService.getAliasToRoomIdMapping(room_alias);
+ console.log("joining " + room_alias + " id=" + room_id);
+ if ($rootScope.events.rooms[room_id]) {
+ // don't send a join event for a room you're already in.
+ $location.url("room/" + room_alias);
+ }
+ else {
+ promise = matrixService.joinAlias(room_alias).then(
+ function(response) {
+ $location.url("room/" + room_alias);
+ },
+ function(error) {
+ $scope.feedback = "Can't join room: " + JSON.stringify(error.data);
+ }
+ );
+ }
+ }
+ }
+ else {
+ $scope.feedback = "Usage: /join <room_alias>";
}
break;
case "/kick":
- // Kick a user from the room
- if (2 === args.length) {
- var user_id = args[1];
+ // Kick a user from the room with an optional reason
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(.*))?$/);
+ if (matches) {
+ promise = matrixService.kick($scope.room_id, matches[1], matches[3]);
+ }
+ }
- // Set his state in the room as leave
- promise = matrixService.setMembership($scope.room_id, user_id, "leave");
+ if (!promise) {
+ $scope.feedback = "Usage: /kick <userId> [<reason>]";
}
break;
-
+
case "/ban":
- // Ban a user from the room
- if (2 <= args.length) {
- // TODO: The user may have entered the display name
- // Need display name -> user_id resolution. Pb: how to manage user with same display names?
- var user_id = args[1];
-
- // Does the user provide a reason?
- if (3 <= args.length) {
- var reason = args.slice(2).join(' ');
+ // Ban a user from the room with an optional reason
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(.*))?$/);
+ if (matches) {
+ promise = matrixService.ban($scope.room_id, matches[1], matches[3]);
}
- promise = matrixService.ban($scope.room_id, user_id, reason);
}
- break;
+ if (!promise) {
+ $scope.feedback = "Usage: /ban <userId> [<reason>]";
+ }
+ break;
+
case "/unban":
// Unban a user from the room
- if (2 === args.length) {
- var user_id = args[1];
-
- // Reset the user membership to leave to unban him
- promise = matrixService.setMembership($scope.room_id, user_id, "leave");
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ // Reset the user membership to "leave" to unban him
+ promise = matrixService.unban($scope.room_id, matches[1]);
+ }
+ }
+
+ if (!promise) {
+ $scope.feedback = "Usage: /unban <userId>";
}
break;
case "/op":
// Define the power level of a user
- if (3 === args.length) {
- var user_id = args[1];
- var powerLevel = parseInt(args[2]);
- promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel);
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(\d+))?$/);
+ var powerLevel = 50; // default power level for op
+ if (matches) {
+ var user_id = matches[1];
+ if (matches.length === 4) {
+ powerLevel = parseInt(matches[3]);
+ }
+ if (powerLevel !== NaN) {
+ promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel);
+ }
+ }
+ }
+
+ if (!promise) {
+ $scope.feedback = "Usage: /op <userId> [<power level>]";
}
break;
case "/deop":
// Reset the power level of a user
- if (2 === args.length) {
- var user_id = args[1];
- promise = matrixService.setUserPowerLevel($scope.room_id, user_id, undefined);
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined);
+ }
}
+
+ if (!promise) {
+ $scope.feedback = "Usage: /deop <userId>";
+ }
+ break;
+
+ default:
+ $scope.feedback = ("Unrecognised IRC-style command: " + cmd);
break;
}
}
- if (!promise) {
- // Send the text message
- promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput);
+ // By default send this as a message unless it's an IRC-style command
+ if (!promise && !cmd) {
+ // Make the request
+ promise = matrixService.sendTextMessage($scope.room_id, line);
+ echo = true;
}
- promise.then(
- function() {
- console.log("Request successfully sent");
- $scope.textInput = "";
- $scope.state.sending = false;
- },
- function(error) {
- $scope.feedback = "Request failed: " + error.data.error;
- $scope.state.sending = false;
- });
+ if (echo) {
+ // Echo the message to the room
+ // To do so, create a minimalist fake text message event and add it to the in-memory list of room messages
+ var echoMessage = {
+ content: {
+ body: (cmd === "/me" ? args : line),
+ hsob_ts: new Date().getTime(), // fake a timestamp
+ msgtype: (cmd === "/me" ? "m.emote" : "m.text"),
+ },
+ room_id: $scope.room_id,
+ type: "m.room.message",
+ user_id: $scope.state.user_id,
+ // FIXME: re-enable echo_msg_state when we have a nice way to turn the field off again
+ // echo_msg_state: "messagePending" // Add custom field to indicate the state of this fake message to HTML
+ };
+
+ $scope.textInput = "";
+ $rootScope.events.rooms[$scope.room_id].messages.push(echoMessage);
+ scrollToBottom();
+ }
+
+ if (promise) {
+ promise.then(
+ function() {
+ console.log("Request successfully sent");
+ $scope.textInput = "";
+/*
+ if (echoMessage) {
+ // Remove the fake echo message from the room messages
+ // It will be replaced by the one acknowledged by the server
+ // ...except this causes a nasty flicker. So don't swap messages for now. --matthew
+ // var index = $rootScope.events.rooms[$scope.room_id].messages.indexOf(echoMessage);
+ // if (index > -1) {
+ // $rootScope.events.rooms[$scope.room_id].messages.splice(index, 1);
+ // }
+ }
+ else {
+ $scope.textInput = "";
+ }
+*/
+ },
+ function(error) {
+ $scope.feedback = "Request failed: " + error.data.error;
+
+ if (echoMessage) {
+ // Mark the message as unsent for the rest of the page life
+ echoMessage.content.hsob_ts = "Unsent";
+ echoMessage.echo_msg_state = "messageUnSent";
+ }
+ });
+ }
};
$scope.onInit = function() {
@@ -531,25 +667,20 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
};
$scope.sendImage = function(url, body) {
- $scope.state.sending = true;
-
+ scrollToBottom(true);
+
matrixService.sendImageMessage($scope.room_id, url, body).then(
function() {
console.log("Image sent");
- $scope.state.sending = false;
},
function(error) {
$scope.feedback = "Failed to send image: " + error.data.error;
- $scope.state.sending = false;
});
};
$scope.imageFileToSend;
$scope.$watch("imageFileToSend", function(newValue, oldValue) {
if ($scope.imageFileToSend) {
-
- $scope.state.sending = true;
-
// Upload this image with its thumbnail to Internet
mFileUpload.uploadImageAndThumbnail($scope.imageFileToSend, THUMBNAIL_SIZE).then(
function(imageMessage) {
@@ -557,16 +688,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
matrixService.sendMessage($scope.room_id, undefined, imageMessage).then(
function() {
console.log("Image message sent");
- $scope.state.sending = false;
},
function(error) {
$scope.feedback = "Failed to send image message: " + error.data.error;
- $scope.state.sending = false;
});
},
function(error) {
$scope.feedback = "Can't upload image";
- $scope.state.sending = false;
}
);
}
@@ -582,6 +710,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
call.onHangup = $rootScope.onCallHangup;
call.placeCall();
$rootScope.currentCall = call;
- }
+ };
}]);
diff --git a/webclient/room/room-directive.js b/webclient/room/room-directive.js
index 659bcbc60f..e033b003e1 100644
--- a/webclient/room/room-directive.js
+++ b/webclient/room/room-directive.js
@@ -48,6 +48,9 @@ angular.module('RoomController')
var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
if (targetIndex === 0) {
element[0].value = text;
+
+ // Force angular to wake up and update the input ng-model by firing up input event
+ angular.element(element[0]).triggerHandler('input');
}
else if (search && search[1]) {
// console.log("search found: " + search);
@@ -81,7 +84,10 @@ angular.module('RoomController')
expansion += " ";
element[0].value = text.replace(/@?([a-zA-Z0-9_\-:\.]+)$/, expansion);
// cancel blink
- element[0].className = "";
+ element[0].className = "";
+
+ // Force angular to wake up and update the input ng-model by firing up input event
+ angular.element(element[0]).triggerHandler('input');
}
else {
// console.log("wrapped!");
@@ -91,6 +97,9 @@ angular.module('RoomController')
}, 150);
element[0].value = text;
scope.tabCompleteIndex = 0;
+
+ // Force angular to wake up and update the input ng-model by firing up input event
+ angular.element(element[0]).triggerHandler('input');
}
}
else {
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 6732a7b3ae..5bd2cc92d5 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -3,7 +3,7 @@
<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 }}
+ {{ room_id | mRoomName }}
</div>
</div>
@@ -40,7 +40,10 @@
ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
<td class="leftBlock">
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
- <div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div>
+ <div class="timestamp"
+ ng-class="msg.echo_msg_state">
+ {{ (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.png' }}" width="32" height="32"
@@ -59,15 +62,24 @@
{{ members[msg.user_id].displayname || msg.user_id }}
{{ {"join": "kicked", "ban": "unbanned"}[msg.content.prev] }}
{{ members[msg.state_key].displayname || msg.state_key }}
+ <span ng-if="'join' === msg.content.prev && msg.content.reason">
+ : {{ msg.content.reason }}
+ </span>
</span>
</span>
<span ng-if="'invite' === msg.content.membership || 'ban' === msg.content.membership">
{{ members[msg.user_id].displayname || msg.user_id }}
{{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
{{ members[msg.state_key].displayname || msg.state_key }}
- </span>
+ <span ng-if="'ban' === msg.content.prev && msg.content.reason">
+ : {{ msg.content.reason }}
+ </span>
+ </span>
+
<span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
- <span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
+ <span ng-show='msg.content.msgtype === "m.text"'
+ ng-class="msg.echo_msg_state"
+ ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
<div ng-show='msg.content.msgtype === "m.image"'>
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
<img class="image" ng-src="{{ msg.content.url }}"/>
diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js
index 7a26367a1b..8c877a24e9 100644
--- a/webclient/settings/settings-controller.js
+++ b/webclient/settings/settings-controller.js
@@ -19,6 +19,17 @@ limitations under the License.
angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInput'])
.controller('SettingsController', ['$scope', 'matrixService', 'mFileUpload',
function($scope, matrixService, mFileUpload) {
+ // XXX: duplicated from register
+ var generateClientSecret = function() {
+ var ret = "";
+ var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+ for (var i = 0; i < 32; i++) {
+ ret += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+
+ return ret;
+ };
$scope.config = matrixService.config();
$scope.profile = {
@@ -106,16 +117,22 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
$scope.linkedEmails = {
linkNewEmail: "", // the email entry box
emailBeingAuthed: undefined, // to populate verification text
- authTokenId: undefined, // the token id from the IS
+ authSid: undefined, // the token id from the IS
emailCode: "", // the code entry box
linkedEmailList: matrixService.config().emailList // linked email list
};
$scope.linkEmail = function(email) {
- matrixService.linkEmail(email).then(
+ if (email != $scope.linkedEmails.emailBeingAuthed) {
+ $scope.linkedEmails.emailBeingAuthed = email;
+ $scope.clientSecret = generateClientSecret();
+ $scope.sendAttempt = 0;
+ }
+ $scope.sendAttempt++;
+ matrixService.linkEmail(email, $scope.clientSecret, $scope.sendAttempt).then(
function(response) {
if (response.data.success === true) {
- $scope.linkedEmails.authTokenId = response.data.tokenId;
+ $scope.linkedEmails.authSid = response.data.sid;
$scope.emailFeedback = "You have been sent an email.";
$scope.linkedEmails.emailBeingAuthed = email;
}
@@ -129,34 +146,44 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
);
};
- $scope.submitEmailCode = function(code) {
- var tokenId = $scope.linkedEmails.authTokenId;
+ $scope.submitEmailCode = function() {
+ var tokenId = $scope.linkedEmails.authSid;
if (tokenId === undefined) {
$scope.emailFeedback = "You have not requested a code with this email.";
return;
}
- matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
+ matrixService.authEmail($scope.clientSecret, $scope.linkedEmails.authSid, $scope.linkedEmails.emailCode).then(
function(response) {
- if ("success" in response.data && response.data.success === false) {
+ if ("errcode" in response.data) {
$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 = "";
+ matrixService.bindEmail(matrixService.config().user_id, tokenId, $scope.clientSecret).then(
+ function(response) {
+ if ('errcode' in response.data) {
+ $scope.emailFeedback = "Failed to link email.";
+ return;
+ }
+ var config = matrixService.config();
+ var emailList = {};
+ if ("emailList" in config) {
+ emailList = config.emailList;
+ }
+ emailList[$scope.linkedEmails.emailBeingAuthed] = response;
+ // save the new email list
+ config.emailList = emailList;
+ matrixService.setConfig(config);
+ matrixService.saveConfig();
+ // invalidate the email being authed and update UI.
+ $scope.linkedEmails.emailBeingAuthed = undefined;
+ $scope.emailFeedback = "";
+ $scope.linkedEmails.linkedEmailList = emailList;
+ $scope.linkedEmails.linkNewEmail = "";
+ $scope.linkedEmails.emailCode = "";
+ }, function(reason) {
+ $scope.emailFeedback = "Failed to link email: " + reason;
+ }
+ );
},
function(reason) {
$scope.emailFeedback = "Failed to auth email: " + reason;
@@ -182,4 +209,4 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
$scope.settings.notifications = permission;
});
};
-}]);
\ No newline at end of file
+}]);
diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html
index b7fd5dfb50..924812e7ae 100644
--- a/webclient/settings/settings.html
+++ b/webclient/settings/settings.html
@@ -23,14 +23,14 @@
</div>
<br/>
- <h3 style="display: none; ">Linked emails</h3>
- <div class="section" style="display: none; ">
+ <h3>Linked emails</h3>
+ <div class="section">
<form>
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
Link Email
</button>
- {{ emailFeedback }}
+ {{ emailFeedback }}
</form>
<form ng-hide="!linkedEmails.emailBeingAuthed">
Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
@@ -81,7 +81,7 @@
<ul>
<li>/nick <display_name>: change your display name</li>
<li>/me <action>: send the action you are doing. /me will be replaced by your display name</li>
- <li>/kick <user_id>: kick the user</li>
+ <li>/kick <user_id> [<reason>]: kick the user</li>
<li>/ban <user_id> [<reason>]: ban the user</li>
<li>/unban <user_id>: unban the user</li>
<li>/op <user_id> <power_level>: set user power level</li>
|