Merge with Matthew's killing of ng-animate

Conflicts:
	syweb/webclient/app-controller.js
	syweb/webclient/index.html
This commit is contained in:
David Baker 2014-11-13 14:37:43 +00:00
commit cdb8d746ef
7 changed files with 329 additions and 174 deletions

View File

@ -27,12 +27,6 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
// Check current URL to avoid to display the logout button on the login page // Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path(); $scope.location = $location.path();
// disable nganimate for the local and remote video elements because ngAnimate appears
// to be buggy and leaves animation classes on the video elements causing them to show
// when they should not (their animations are pure CSS3)
//$animate.enabled(false, angular.element('#localVideo'));
//$animate.enabled(false, angular.element('#remoteVideo'));
// Update the location state when the ng location changed // Update the location state when the ng location changed
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) { $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
$scope.location = $location.path(); $scope.location = $location.path();

View File

@ -32,6 +32,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
'notificationService', 'notificationService',
'recentsService', 'recentsService',
'modelService', 'modelService',
'commandsService',
'infinite-scroll', 'infinite-scroll',
'ui.bootstrap', 'ui.bootstrap',
'monospaced.elastic' 'monospaced.elastic'

View File

@ -0,0 +1,164 @@
/*
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';
/*
This service contains logic for parsing and performing IRC style commands.
*/
angular.module('commandsService', [])
.factory('commandsService', ['$q', '$location', 'matrixService', 'modelService', function($q, $location, matrixService, modelService) {
// create a rejected promise with the given message
var reject = function(msg) {
var deferred = $q.defer();
deferred.reject({
data: {
error: msg
}
});
return deferred.promise;
};
// Change your nickname
var doNick = function(room_id, args) {
if (args) {
return matrixService.setDisplayName(args);
}
return reject("Usage: /nick <display_name>");
};
// Join a room
var doJoin = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
var room_alias = matches[1];
$location.url("room/" + room_alias);
// NB: We don't need to actually do the join, since that happens
// automatically if we are not joined onto a room already when
// the page loads.
return reject("Joining "+room_alias);
}
}
return reject("Usage: /join <room_alias>");
};
// Kick a user from the room with an optional reason
var doKick = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
return matrixService.kick(room_id, matches[1], matches[3]);
}
}
return reject("Usage: /kick <userId> [<reason>]");
};
// Ban a user from the room with an optional reason
var doBan = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
return matrixService.ban(room_id, matches[1], matches[3]);
}
}
return reject("Usage: /ban <userId> [<reason>]");
};
// Unban a user from the room
var doUnban = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
// Reset the user membership to "leave" to unban him
return matrixService.unban(room_id, matches[1]);
}
}
return reject("Usage: /unban <userId>");
};
// Define the power level of a user
var doOp = function(room_id, args) {
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 && undefined !== matches[3]) {
powerLevel = parseInt(matches[3]);
}
if (powerLevel !== NaN) {
var powerLevelEvent = modelService.getRoom(room_id).current_room_state.state("m.room.power_levels");
return matrixService.setUserPowerLevel(room_id, user_id, powerLevel, powerLevelEvent);
}
}
}
return reject("Usage: /op <userId> [<power level>]");
};
// Reset the power level of a user
var doDeop = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
var powerLevelEvent = modelService.getRoom(room_id).current_room_state.state("m.room.power_levels");
return matrixService.setUserPowerLevel(room_id, args, undefined, powerLevelEvent);
}
}
return reject("Usage: /deop <userId>");
};
var commands = {
"nick": doNick,
"join": doJoin,
"kick": doKick,
"ban": doBan,
"unban": doUnban,
"op": doOp,
"deop": doDeop
};
return {
/**
* Process the given text for commands and perform them.
* @param {String} roomId The room in which the input was performed.
* @param {String} input The raw text input by the user.
* @return {Promise} A promise of the pending command, or null if the
* input is not a command.
*/
processInput: function(roomId, input) {
// trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands
input = input.replace(/\s+$/, "");
if (input[0] === "/" && input[1] !== "/") {
var bits = input.match(/^(\S+?)( +(.*))?$/);
var cmd = bits[1].substring(1);
var args = bits[3];
if (commands[cmd]) {
return commands[cmd](roomId, args);
}
return reject("Unrecognised IRC-style command: " + cmd);
}
return null; // not a command
}
};
}]);

View File

@ -17,8 +17,8 @@ limitations under the License.
'use strict'; 'use strict';
angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController']) angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
.controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService', 'modelService', .controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService', 'modelService', 'recentsService',
function($scope, $location, matrixService, eventHandlerService, modelService) { function($scope, $location, matrixService, eventHandlerService, modelService, recentsService) {
$scope.config = matrixService.config(); $scope.config = matrixService.config();
$scope.public_rooms = []; $scope.public_rooms = [];
@ -47,6 +47,8 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
user: "" user: ""
}; };
recentsService.setSelectedRoomId(undefined);
var refresh = function() { var refresh = function() {
matrixService.publicRooms().then( matrixService.publicRooms().then(

View File

@ -44,6 +44,7 @@
<script src="components/matrix/event-handler-service.js"></script> <script src="components/matrix/event-handler-service.js"></script>
<script src="components/matrix/notification-service.js"></script> <script src="components/matrix/notification-service.js"></script>
<script src="components/matrix/recents-service.js"></script> <script src="components/matrix/recents-service.js"></script>
<script src="components/matrix/commands-service.js"></script>
<script src="components/matrix/model-service.js"></script> <script src="components/matrix/model-service.js"></script>
<script src="components/matrix/presence-service.js"></script> <script src="components/matrix/presence-service.js"></script>
<script src="components/fileInput/file-input-directive.js"></script> <script src="components/fileInput/file-input-directive.js"></script>

View File

@ -15,8 +15,8 @@ limitations under the License.
*/ */
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'angular-peity']) angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'angular-peity'])
.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService', 'recentsService', .controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService', 'recentsService', 'commandsService',
function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService, recentsService) { function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService, recentsService, commandsService) {
'use strict'; 'use strict';
var MESSAGES_PER_PAGINATION = 30; var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320; var THUMBNAIL_SIZE = 320;
@ -435,172 +435,22 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
// Store the command in the history // Store the command in the history
history.push(input); history.push(input);
var isEmote = input.indexOf("/me ") === 0;
var promise; var promise;
var cmd; if (!isEmote) {
var args; promise = commandsService.processInput($scope.room_id, input);
}
var echo = false; var echo = false;
// Check for IRC style commands first
// trim any trailing whitespace, as it can confuse the parser for IRC-style commands
input = input.replace(/\s+$/, "");
if (input[0] === "/" && input[1] !== "/") { if (!promise) { // not a non-echoable command
var bits = input.match(/^(\S+?)( +(.*))?$/);
cmd = bits[1];
args = bits[3];
console.log("cmd: " + cmd + ", args: " + args);
switch (cmd) {
case "/me":
promise = matrixService.sendEmoteMessage($scope.room_id, args);
echo = true;
break;
case "/nick":
// Change user display name
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 = modelService.getAliasToRoomIdMapping(room_alias);
console.log("joining " + room_alias + " id=" + room_id);
if ($scope.room) { // TODO actually check that you = join
// 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) {
// TODO: factor out the common housekeeping whenever we try to join a room or alias
matrixService.roomState(response.room_id).then(
function(response) {
eventHandlerService.handleEvents(response.data, false, true);
},
function(error) {
$scope.feedback = "Failed to get room state for: " + response.room_id;
}
);
$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 with an optional reason
if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
promise = matrixService.kick($scope.room_id, matches[1], matches[3]);
}
}
if (!promise) {
$scope.feedback = "Usage: /kick <userId> [<reason>]";
}
break;
case "/ban":
// 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]);
}
}
if (!promise) {
$scope.feedback = "Usage: /ban <userId> [<reason>]";
}
break;
case "/unban":
// Unban a user from the room
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 (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 && undefined !== matches[3]) {
powerLevel = parseInt(matches[3]);
}
if (powerLevel !== NaN) {
var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels");
promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel, powerLevelEvent);
}
}
}
if (!promise) {
$scope.feedback = "Usage: /op <userId> [<power level>]";
}
break;
case "/deop":
// Reset the power level of a user
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels");
promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined, powerLevelEvent);
}
}
if (!promise) {
$scope.feedback = "Usage: /deop <userId>";
}
break;
default:
$scope.feedback = ("Unrecognised IRC-style command: " + cmd);
break;
}
}
// 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, input);
echo = true; echo = true;
if (isEmote) {
promise = matrixService.sendEmoteMessage($scope.room_id, input.substring(4));
}
else {
promise = matrixService.sendTextMessage($scope.room_id, input);
}
} }
if (echo) { if (echo) {
@ -608,8 +458,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
// To do so, create a minimalist fake text message event and add it to the in-memory list of room messages // To do so, create a minimalist fake text message event and add it to the in-memory list of room messages
var echoMessage = { var echoMessage = {
content: { content: {
body: (cmd === "/me" ? args : input), body: (isEmote ? input.substring(4) : input),
msgtype: (cmd === "/me" ? "m.emote" : "m.text"), msgtype: (isEmote ? "m.emote" : "m.text"),
}, },
origin_server_ts: new Date().getTime(), // fake a timestamp origin_server_ts: new Date().getTime(), // fake a timestamp
room_id: $scope.room_id, room_id: $scope.room_id,
@ -642,7 +492,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
} }
}, },
function(error) { function(error) {
$scope.feedback = "Request failed: " + error.data.error; $scope.feedback = error.data.error;
if (echoMessage) { if (echoMessage) {
// Mark the message as unsent for the rest of the page life // Mark the message as unsent for the rest of the page life

View File

@ -0,0 +1,143 @@
describe('CommandsService', function() {
var scope;
var roomId = "!dlwifhweu:localhost";
var testPowerLevelsEvent, testMatrixServicePromise;
var matrixService = { // these will be spyed on by jasmine, hence stub methods
setDisplayName: function(args){},
kick: function(args){},
ban: function(args){},
unban: function(args){},
setUserPowerLevel: function(args){}
};
var modelService = {
getRoom: function(roomId) {
return {
room_id: roomId,
current_room_state: {
events: {
"m.room.power_levels": testPowerLevelsEvent
},
state: function(type, key) {
return key ? this.events[type+key] : this.events[type];
}
}
};
}
};
// helper function for asserting promise outcomes
NOTHING = "[Promise]";
RESOLVED = "[Resolved promise]";
REJECTED = "[Rejected promise]";
var expectPromise = function(promise, expects) {
var value = NOTHING;
promise.then(function(result) {
value = RESOLVED;
}, function(fail) {
value = REJECTED;
});
scope.$apply();
expect(value).toEqual(expects);
};
// setup the service and mocked dependencies
beforeEach(function() {
// set default mock values
testPowerLevelsEvent = {
content: {
default: 50
},
user_id: "@foo:bar",
room_id: roomId
}
// mocked dependencies
module(function ($provide) {
$provide.value('matrixService', matrixService);
$provide.value('modelService', modelService);
});
// tested service
module('commandsService');
});
beforeEach(inject(function($rootScope, $q) {
scope = $rootScope;
testMatrixServicePromise = $q.defer();
}));
it('should reject a no-arg "/nick".', inject(
function(commandsService) {
var promise = commandsService.processInput(roomId, "/nick");
expectPromise(promise, REJECTED);
}));
it('should be able to set a /nick with multiple words.', inject(
function(commandsService) {
spyOn(matrixService, 'setDisplayName').and.returnValue(testMatrixServicePromise);
var promise = commandsService.processInput(roomId, "/nick Bob Smith");
expect(matrixService.setDisplayName).toHaveBeenCalledWith("Bob Smith");
expect(promise).toBe(testMatrixServicePromise);
}));
it('should be able to /kick a user without a reason.', inject(
function(commandsService) {
spyOn(matrixService, 'kick').and.returnValue(testMatrixServicePromise);
var promise = commandsService.processInput(roomId, "/kick @bob:matrix.org");
expect(matrixService.kick).toHaveBeenCalledWith(roomId, "@bob:matrix.org", undefined);
expect(promise).toBe(testMatrixServicePromise);
}));
it('should be able to /kick a user with a reason.', inject(
function(commandsService) {
spyOn(matrixService, 'kick').and.returnValue(testMatrixServicePromise);
var promise = commandsService.processInput(roomId, "/kick @bob:matrix.org he smells");
expect(matrixService.kick).toHaveBeenCalledWith(roomId, "@bob:matrix.org", "he smells");
expect(promise).toBe(testMatrixServicePromise);
}));
it('should be able to /ban a user without a reason.', inject(
function(commandsService) {
spyOn(matrixService, 'ban').and.returnValue(testMatrixServicePromise);
var promise = commandsService.processInput(roomId, "/ban @bob:matrix.org");
expect(matrixService.ban).toHaveBeenCalledWith(roomId, "@bob:matrix.org", undefined);
expect(promise).toBe(testMatrixServicePromise);
}));
it('should be able to /ban a user with a reason.', inject(
function(commandsService) {
spyOn(matrixService, 'ban').and.returnValue(testMatrixServicePromise);
var promise = commandsService.processInput(roomId, "/ban @bob:matrix.org he smells");
expect(matrixService.ban).toHaveBeenCalledWith(roomId, "@bob:matrix.org", "he smells");
expect(promise).toBe(testMatrixServicePromise);
}));
it('should be able to /unban a user.', inject(
function(commandsService) {
spyOn(matrixService, 'unban').and.returnValue(testMatrixServicePromise);
var promise = commandsService.processInput(roomId, "/unban @bob:matrix.org");
expect(matrixService.unban).toHaveBeenCalledWith(roomId, "@bob:matrix.org");
expect(promise).toBe(testMatrixServicePromise);
}));
it('should be able to /op a user.', inject(
function(commandsService) {
spyOn(matrixService, 'setUserPowerLevel').and.returnValue(testMatrixServicePromise);
var promise = commandsService.processInput(roomId, "/op @bob:matrix.org 50");
expect(matrixService.setUserPowerLevel).toHaveBeenCalledWith(roomId, "@bob:matrix.org", 50, testPowerLevelsEvent);
expect(promise).toBe(testMatrixServicePromise);
}));
it('should be able to /deop a user.', inject(
function(commandsService) {
spyOn(matrixService, 'setUserPowerLevel').and.returnValue(testMatrixServicePromise);
var promise = commandsService.processInput(roomId, "/deop @bob:matrix.org");
expect(matrixService.setUserPowerLevel).toHaveBeenCalledWith(roomId, "@bob:matrix.org", undefined, testPowerLevelsEvent);
expect(promise).toBe(testMatrixServicePromise);
}));
});