diff --git a/.eslintrc.json b/.eslintrc.json index 7610d2b2..81b8f889 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,10 +25,6 @@ 2, "single" ], - "linebreak-style": [ - 2, - "unix" - ], "semi": [ 2, "always" diff --git a/bower.json b/bower.json index 411e0c5e..0f2dd8cb 100644 --- a/bower.json +++ b/bower.json @@ -15,5 +15,7 @@ "clipboard": "~1.5.3", "wsevent.js": "~0.0.4" }, - "devDependencies": {} + "devDependencies": { + "angular-mocks": "~1.4.7" + } } diff --git a/circle.yml b/circle.yml index 0be1a8e2..8af05dc3 100644 --- a/circle.yml +++ b/circle.yml @@ -10,6 +10,7 @@ dependencies: test: override: - gulp lint + - gulp test deployment: master: branch: master diff --git a/gulp/build.js b/gulp/build.js index a4e6a42b..04ffc4db 100644 --- a/gulp/build.js +++ b/gulp/build.js @@ -1,6 +1,10 @@ 'use strict'; +var fs = require('fs'); +var glob = require('glob'); var path = require('path'); +var _ = require('lodash'); +var runSequence = require('run-sequence'); var gulp = require('gulp'); var conf = require('./conf'); @@ -109,8 +113,24 @@ gulp.task('clean', function (done) { $.del([path.join(conf.paths.dist, '/'), path.join(conf.paths.tmp, '/')], done); }); -gulp.task('build-after-cleaned', ['html', 'fonts', 'other']); +// Return only base file name without dir +function getMostRecentMtimeSync(dir) { + var files = glob.sync(path.join(dir, '**/*')); -gulp.task('build', ['clean'], function () { - gulp.start('build-after-cleaned'); + return _.max(_.map(files, function (f) { + return fs.statSync(f).mtime; + })); +} + +gulp.task('rebuild', function (cb) { + runSequence('clean', ['html', 'fonts', 'other'], cb); +}); + +gulp.task('build', function (cb) { + if (getMostRecentMtimeSync(conf.paths.src) <= getMostRecentMtimeSync(conf.paths.dist)) { + $.util.log('No modified files: not building'); + cb(); + } else { + runSequence('rebuild', cb); + } }); diff --git a/gulp/conf.js b/gulp/conf.js index c4639f1b..94312a8a 100644 --- a/gulp/conf.js +++ b/gulp/conf.js @@ -14,6 +14,7 @@ var gutil = require('gulp-util'); exports.paths = { src: 'src', dist: 'dist', + test: 'test', tmp: '.tmp', e2e: 'e2e' }; diff --git a/gulp/test.js b/gulp/test.js new file mode 100644 index 00000000..68519eb5 --- /dev/null +++ b/gulp/test.js @@ -0,0 +1,35 @@ +'use strict'; + +var path = require('path'); +var runSequence = require('run-sequence'); +var mainBowerFiles = require('main-bower-files'); +var gulp = require('gulp'); +var conf = require('./conf'); +var karma = require('karma'); + +var $ = require('gulp-load-plugins')(); + +gulp.task('test:unit', function (done) { + var server = new karma.Server({ + browsers: ['PhantomJS'], + frameworks: ['mocha', 'chai-sinon'], + files: mainBowerFiles({ includeDev: true }).concat([ + 'dist/scripts/app-*.js', + 'test/karma/**/*.js' + ]), + logLevel: 'DEBUG', + singleRun: true + }); + + server.on('run_complete', function (browsers, results) { + // NB If the argument of done() is not null or not undefined, + // e.g. a string, the next task in a series won't run. + done(results.error ? 'There are test failures' : null); + }); + + server.start(); +}); + +gulp.task('test', function (cb) { + runSequence('build', 'test:unit', cb); +}); diff --git a/package.json b/package.json index 41337645..bb7e10c8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "GPL-3.0", "repository": "https://github.com/TF2Stadium/Frontend", "readme": "readme.md", - "version": "0.3.1", + "version": "0.3.2", "dependencies": { "del": "~1.2.0", "uglify-save-license": "~0.4.1", @@ -36,18 +36,32 @@ "devDependencies": { "browser-sync": "~2.7.12", "browser-sync-spa": "~1.0.2", + "chai": "^3.4.1", + "chai-as-promised": "^5.1.0", "chalk": "^1.1.1", "concat-stream": "~1.5.0", "connect-history-api-fallback": "^1.1.0", "eslint": "^1.10.1", "eslint-plugin-angular": "^0.14.0", + "glob": "^6.0.1", "gulp-eslint": "^1.1.1", + "gulp-karma": "0.0.5", + "gulp-mocha": "^2.2.0", "gulp-protractor": "~1.0.0", "gulp-rename": "~1.2.2", "gulp-replace": "~0.5.3", "http-proxy-middleware": "~0.0.5", + "karma": "^0.13.15", + "karma-chai-sinon": "^0.1.5", + "karma-mocha": "^0.2.1", + "karma-phantomjs-launcher": "^0.2.1", "merge-stream": "~0.1.7", - "require-dir": "~0.3.0" + "mocha": "^2.3.4", + "phantomjs": "^1.9.19", + "require-dir": "~0.3.0", + "run-sequence": "^1.1.5", + "selenium-webdriver": "^2.48.2", + "sinon": "^1.17.2" }, "engines": { "node": ">=0.10.0" diff --git a/src/app/app.run.js b/src/app/app.run.js index 3561b140..c8694ef9 100644 --- a/src/app/app.run.js +++ b/src/app/app.run.js @@ -38,6 +38,7 @@ $rootScope.config = Config; Settings.getSettings(function (settings) { $rootScope.currentTheme = settings.currentTheme; + $rootScope.currentTimestampsOption = settings.timestamps; $rootScope.themeLoaded = true; }); diff --git a/src/app/app.settings.js b/src/app/app.settings.js index d4f8c283..aef30b4c 100644 --- a/src/app/app.settings.js +++ b/src/app/app.settings.js @@ -53,12 +53,19 @@ dark: {name: 'TF2Stadium Dark', selector: 'dark-theme'} }; + SettingsProvider.constants.timestampOptions = { + hours12: {name: '12-hour'}, + hours24: {name: '24-hour'}, + none: {name: 'None'} + }; + SettingsProvider.constants.sound = { soundVolume: {name: 'Notifications volume'} }; function setDefaultValues() { SettingsProvider.settings.currentTheme = 'default-theme'; + SettingsProvider.settings.timestamps = 'hours12'; /* Defaults every value found in the filters to true. diff --git a/src/app/pages/settings/section-theme.html b/src/app/pages/settings/section-theme.html index 8d8f7186..c7215968 100644 --- a/src/app/pages/settings/section-theme.html +++ b/src/app/pages/settings/section-theme.html @@ -1,11 +1,22 @@

Theme

- {{settings.current}} - + {{theme.name}} -
\ No newline at end of file + +

Chat

+

Timestamps

+ + + {{opt.name}} + + + diff --git a/src/app/pages/settings/settings-sidebar.html b/src/app/pages/settings/settings-sidebar.html index 1feb912c..f87f01e2 100644 --- a/src/app/pages/settings/settings-sidebar.html +++ b/src/app/pages/settings/settings-sidebar.html @@ -4,7 +4,7 @@

{{::settingSection}} diff --git a/src/app/pages/settings/settings.provider.js b/src/app/pages/settings/settings.provider.js index 037b8dab..b8791676 100644 --- a/src/app/pages/settings/settings.provider.js +++ b/src/app/pages/settings/settings.provider.js @@ -35,13 +35,18 @@ var settingsPageProvider = {}; - settingsPageProvider.sections = []; + settingsPageProvider.sections = {}; /** @ngInject */ var settingsPageService = function (Settings) { - settingsPageProvider.sections.theme = Settings.getConstants('themesList'); - settingsPageProvider.sections.filters = Settings.getConstants('filters'); - settingsPageProvider.sections.sound = Settings.getConstants('sound'); + settingsPageProvider.sections = { + theme: { + theme: Settings.getConstants('themesList'), + timestamps: Settings.getConstants('timestampOptions') + }, + filters: Settings.getConstants('filters'), + sound: Settings.getConstants('sound') + }; settingsPageService.getSections = function () { return settingsPageProvider.sections; diff --git a/src/app/shared/comment-box/chat.service.js b/src/app/shared/comment-box/chat.service.js index 5aaadddc..29cb0fa8 100644 --- a/src/app/shared/comment-box/chat.service.js +++ b/src/app/shared/comment-box/chat.service.js @@ -4,33 +4,33 @@ angular.module('tf2stadium.services') .factory('ChatService', ChatService); - // Persistent map of room id -> messages list - var chatRoomLogs = Object.create(null); - function getChatRoom(id) { - if (angular.isUndefined(chatRoomLogs[id])) { - chatRoomLogs[id] = []; - } - return chatRoomLogs[id]; - } + /** @ngInject */ + function ChatService(Websocket, $rootScope, LobbyService) { + var factory = {}; - function ChatRoom(id) { - this.changeRoom(angular.isDefined(id)? id : -1); - } + // Persistent map of room id -> messages list + var chatRoomLogs = Object.create(null); + function getChatRoom(id) { + if (angular.isUndefined(chatRoomLogs[id])) { + chatRoomLogs[id] = []; + } + return chatRoomLogs[id]; + } - ChatRoom.prototype.changeRoom = function chageRoom(id) { - if (id !== this.id) { - this.id = id; - this.messages = getChatRoom(id); + function ChatRoom(id) { + this.changeRoom(angular.isDefined(id)? id : -1); } - }; - ChatRoom.prototype.leave = function leave() { - this.changeRoom(-1); - }; + ChatRoom.prototype.changeRoom = function chageRoom(id) { + if (id !== this.id) { + this.id = id; + this.messages = getChatRoom(id); + } + }; - /** @ngInject */ - function ChatService(Websocket, $rootScope, LobbyService) { - var factory = {}; + ChatRoom.prototype.leave = function leave() { + this.changeRoom(-1); + }; var globalChatRoom = new ChatRoom(0); @@ -67,11 +67,34 @@ }); Websocket.onJSON('chatReceive', function (message) { - getChatRoom(message.room).push(message); + message.timestamp = new Date(message.timestamp * 1000); + + var log = getChatRoom(message.room); + + // Insert messages in sorted order (sorted by message id) + if (log.length === 0 || log[log.length - 1].id < message.id) { + log.push(message); + } else { + // performance likely isn't an issue, but since the log is + // sorted by id, it would be better to use a binary search + // here (also, use ES6 findIndex when available). + var insertIdx = 0; + while (log[insertIdx].id < message.id) { + insertIdx++; + } + if (log[insertIdx].id === message.id) { + // Same message id? Overwrite the logged message + log[insertIdx] = message; + } else { + // else insert it into the array (yeah, splice is far from + // efficient, but this should be very rare). + log.splice(insertIdx, 0, message); + } + } + $rootScope.$emit('chat-message', message); }); - Websocket.onJSON('chatHistoryClear', function (data) { // Note: ChatRooms may have pointers to the arrays in // chatRoomLogs, so we have to mutate the actual logs rather diff --git a/src/app/shared/comment-box/comment-box.html b/src/app/shared/comment-box/comment-box.html index 1694f148..f0b2c620 100644 --- a/src/app/shared/comment-box/comment-box.html +++ b/src/app/shared/comment-box/comment-box.html @@ -22,6 +22,8 @@
+ {{message.timestamp | date:($root.currentTimestampsOption === 'hours12'? 'shortTime' : 'H:mm')}}