diff --git a/app/views/includes/foot.jade b/app/views/includes/foot.jade index 378c7cd..6168a11 100755 --- a/app/views/includes/foot.jade +++ b/app/views/includes/foot.jade @@ -18,6 +18,8 @@ script(type='text/javascript', src='https://code.angularjs.org/1.1.5/angular-coo script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap-tpls.js') script(type='text/javascript', src='/lib/angular-ui-utils/modules/route/route.js') +//Firebase +script(type='text/javascript' src='lib/firebase/firebase.js') //Application Init script(type='text/javascript', src='/js/init.js') script(type='text/javascript', src='/js/app.js') @@ -29,12 +31,14 @@ script(type='text/javascript', src='/js/services/global.js') script(type='text/javascript', src='/js/services/socket.js') script(type='text/javascript', src='/js/services/game.js') script(type='text/javascript', src='/js/services/history.fac.js') +script(type='text/javascript', src='/js/services/game-chat.js') //Application Controllers script(type='text/javascript', src='/js/controllers/index.js') script(type='text/javascript', src='/js/controllers/header.js') script(type='text/javascript', src='/js/controllers/game.js') script(type='text/javascript', src='/js/controllers/history.ctr.js') +script(type='text/javascript', src='/js/controllers/game-chat.js') script(type='text/javascript', src='/js/init.js') //Socket.io Client Library diff --git a/app/views/includes/head.jade b/app/views/includes/head.jade index 43df258..07e60e1 100755 --- a/app/views/includes/head.jade +++ b/app/views/includes/head.jade @@ -24,6 +24,7 @@ head link(rel='stylesheet', href='/lib/bootstrap/dist/css/bootstrap.css') link(rel='stylesheet', href='/css/landing.css') + link(rel='stylesheet', href='/css/chat.css') //if lt IE 9 diff --git a/bower.json b/bower.json index d665f73..8edd51f 100755 --- a/bower.json +++ b/bower.json @@ -10,7 +10,8 @@ "angular": "^1.5.8", "angular-mocks": "^1.5.8", "angular-resource": "^1.5.8", - "jasmine-jquery": "^2.1.1" + "jasmine-jquery": "^2.1.1", + "firebase": "^3.4.1" }, "exportsOverride": { "bootstrap": { diff --git a/gulpfile.js b/gulpfile.js index bbbfa43..411808e 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -22,7 +22,7 @@ gulp.task('karma',function (done) { }); gulp.task('sass', function(){ - return gulp.src('public/css/common.scss') + return gulp.src('public/css/*.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('public/css/')); }); diff --git a/karma.conf.js b/karma.conf.js index 4039295..ca16d32 100755 --- a/karma.conf.js +++ b/karma.conf.js @@ -25,6 +25,8 @@ module.exports = function(config) { 'public/lib/angular/angular.js', 'public/lib/angular-resource/angular-resource.js', 'public/lib/angular-mocks/angular-mocks.js', + 'public/lib/firebase/firebase.js', + 'public/js/socket.io.js', './src/app/**/*.js', './src/app/*.js', 'public/js/**/*.js', diff --git a/public/css/chat.scss b/public/css/chat.scss new file mode 100644 index 0000000..14f2e62 --- /dev/null +++ b/public/css/chat.scss @@ -0,0 +1,150 @@ +@import "variable"; + +.chat-box { + background: $main-color; + bottom: 0; + color: $primary-text; + height: 100%; + position: absolute; + right: 0; + text-align: center; + width: 100%; + + .box { + background: $main-color; + border: thin solid $ascent; + border-radius: $box-radius; + bottom: 2px; + height: 360px; + position: absolute; + width: 300px; + z-index: 1; + } + + .chat-messages { + height: 280px; + overflow-y: scroll; + } + + .chat-top { + background: $ascent; + border-color: $secondary-text; + color: $main-color; + height: 20px; + top: 0; + } + + .chat { + background: $url-0; + border: thin solid; + border-bottom: 0; + border-radius: $box-radius; + bottom: 0; + color: $primary-text; + height: 35px; + position: absolute; + width: 300px; + z-index: 2; + } + + .chat-bottom { + background-color: $main-color; + bottom: 5px; + box-shadow: ($grey-shadow-radius, $grey-shadow-75); + display: inline-block; + height: 35px; + padding: $chat-bottom-padding; + position: absolute; + right: 0; + text-align: left; + width: 297px; + } + + .message { + display: inline-block; + height: 35px; + padding-left: 5px; + padding-right: 5px; + position: relative; + top: 0; + width: 220px; + } +} + +.close-chat { + background: $ascent; + border: thin solid $ascent; + border-width: thin; + bottom: 0; + height: 2px; + position: absolute; + width: 300px; + z-index: 1; +} + +li { + &.chat-li { + border-bottom: 1px; + border-bottom-style: solid; + border-bottom-width: thin; + border-color: $secondary-light; + display: inline-block; + margin-bottom: 5px; + min-height: 50px; + width: 270px; + } + + img { + float: left; + margin-right: 10px; + width: 42px; + } +} + +ul { + &.chat-ul { + padding-left: 10px; + padding-right: 5px; + text-align: left; + } +} + +.chat-side-text { + text-align: left; + width: 270px; +} + + +.chat-name { + font-size: 15px; + font-style: oblique; + margin: 0; + padding: 0; + text-align: left; +} + +.chat-text { + color: $secondary-text; + font-size: 12px; + margin: 0; + overflow-x: auto; + padding: 0; + text-align: left; +} + +.chat-time { + color: $secondary-text; + font-size: 10px; + margin-left: 5px; +} + +.is-typing { + color: $secondary-text; + float: left; + font-size: 10px; +} + +.chat-sign { + display: block; + padding: 2px; +} diff --git a/public/css/variable.scss b/public/css/variable.scss new file mode 100644 index 0000000..8b88113 --- /dev/null +++ b/public/css/variable.scss @@ -0,0 +1,14 @@ +$main-color: #fff; +$primary-text: #000; +$secondary-text: #444; +$ascent: #006ce5; +$secondary-light: #d6d6d6; +$black-shadow-radius: 0 -2px 5px 0; +$black-shadow-75: rgba(0, 0, 0, 0.75); +$grey-shadow-radius: 0 -1px 2px 0; +$grey-shadow-75: rgba(65, 65, 65, 0.75); +$box-radius: 5px 5px 0 0; +$chat-bottom-padding: 0 5px 0 10px; + +$url-0: url("../img/chatnone1.png"); +$url-1: url("../img/chatnone.png"); diff --git a/public/img/chatnone.png b/public/img/chatnone.png new file mode 100644 index 0000000..43ee25c Binary files /dev/null and b/public/img/chatnone.png differ diff --git a/public/img/chatnone1.png b/public/img/chatnone1.png new file mode 100644 index 0000000..8a3a27a Binary files /dev/null and b/public/img/chatnone1.png differ diff --git a/public/img/down2.png b/public/img/down2.png new file mode 100644 index 0000000..70b04b0 Binary files /dev/null and b/public/img/down2.png differ diff --git a/public/img/send.png b/public/img/send.png new file mode 100644 index 0000000..39c0c26 Binary files /dev/null and b/public/img/send.png differ diff --git a/public/img/smile1.png b/public/img/smile1.png new file mode 100644 index 0000000..2ce1b6d Binary files /dev/null and b/public/img/smile1.png differ diff --git a/public/js/app.js b/public/js/app.js index e93ede2..1e205ad 100755 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,4 +1,4 @@ -angular.module('mean', ['ngCookies', 'ngResource', 'ui.bootstrap', 'ui.route', 'mean.system', 'mean.directives', 'services.History']) +angular.module('mean', ['ngCookies', 'ngResource', 'ui.bootstrap', 'ui.route', 'mean.system', 'mean.directives', 'services.History', 'mean.gameChat']) .config(['$routeProvider', function($routeProvider) { $routeProvider. @@ -23,6 +23,9 @@ angular.module('mean', ['ngCookies', 'ngResource', 'ui.bootstrap', 'ui.route', ' when('/choose-avatar', { templateUrl: '/views/choose-avatar.html' }). + when('/game', { + templateUrl: '/views/game.tpl.html' + }). when('/history', { templateUrl: '/views/history.tpl.html' }). @@ -53,4 +56,5 @@ angular.module('mean', ['ngCookies', 'ngResource', 'ui.bootstrap', 'ui.route', ' angular.module('mean.system', []); angular.module('mean.directives', []); -angular.module('services.History', ['mean.system']); \ No newline at end of file +angular.module('services.History', ['mean.system']); +angular.module('mean.gameChat', ['mean.system']); \ No newline at end of file diff --git a/public/js/controllers/game-chat.js b/public/js/controllers/game-chat.js new file mode 100644 index 0000000..e3bd677 --- /dev/null +++ b/public/js/controllers/game-chat.js @@ -0,0 +1,106 @@ +'use strict'; +angular.module('mean.gameChat') + .controller('GameChatCtrl', function ($scope, GameChat, game, Global) { + $scope.gameId = game.gameID; + $scope.users = game.players; + $scope.currentUser = Global.user; + + $scope.GameChat = GameChat; + $scope.messages = {}; + + $scope.newMessage = function(messages) { + $scope.messages = messages; + $scope.$apply(); + }; + + $scope.sendMessage = function() { + return $scope.GameChat.sendMessage($scope.message, $scope.gameId, $scope.currentUser); + }; + + //triggerIsTyping : function($scope.gameId, typist){ + $scope.showIsTyping = function(){ + //we need to make the user unique for same user so the listener is triggered + $scope.currentUser.rand = Math.ceil(Math.random() * 10000); + $scope.GameChat.triggerIsTyping($scope.gameId, $scope.currentUser); + }; + + var firstCheck = false, waitTime = 2, timer; + $scope.typingListener = function(typist){ + + if (firstCheck) { + waitTime = 2; + clearInterval(timer); + if ($scope.currentUser.name !== typist.name) { + $('#isTyping').html(typist.name + ' is typing...'); + } + + timer = setInterval(function() { + waitTime--; + if(waitTime === 0){ + $('#isTyping').html(''); + clearInterval(timer); + } + }, 1000); + + } + firstCheck = true; + }; + + /* + *Checks if a game chat session already exists + */ + $scope.GameChat.sessionExists($scope.gameId) + .then(function() { + // Loads message from that section + $scope.GameChat.loadMessages($scope.gameId) + .then(function(messages) { + $scope.messages = messages; + $scope.$apply(); + //Listen for new incoming message + $scope.GameChat.listenForMessage($scope.gameId, function(messages){ + $scope.newMessage(messages); + }); + + //Listen for user typing + $scope.GameChat.listenForTyping($scope.gameId, $scope.typingListener); + }); + }) + .catch(function(){ + //No game chat session existed before + $scope.GameChat.startSession($scope.gameId, $scope.users).then(function(){ + //now listen for messages + $scope.GameChat.listenForMessage($scope.gameId, function(messages){ + $scope.newMessage(messages); + }); + + //Listen for user typing + $scope.GameChat.listenForTyping($scope.gameId, $scope.typingListener); + }); + }); + + $(document).ready( function () { + var scrolled = 100000, messageInput = $('#message'); + messageInput.keyup(function(){ + $scope.showIsTyping(); + }); + + $('#send').click( function () { + $('#message').val(''); + $('#chatMessages').animate({ + scrollTop: scrolled + }); + }); + + messageInput.keypress(function (e) { + if(e.which === 13) { + $scope.sendMessage(); + $('#message').val(''); + $('#chatMessages').animate({ + scrollTop: scrolled + }); + } + }); + + }); + + }); \ No newline at end of file diff --git a/public/js/services/game-chat.js b/public/js/services/game-chat.js new file mode 100644 index 0000000..792d8f3 --- /dev/null +++ b/public/js/services/game-chat.js @@ -0,0 +1,128 @@ +/** + * Created by cyrielo on 10/11/16. + */ +'use strict'; +angular.module('mean.gameChat') + .factory('GameChat', function () { + var config = { + apiKey: 'AIzaSyAO8Mno98d8V0V0ECnuTxCL4jGIYhHDKDc', + authDomain: 'project-kinsan.firebaseapp.com', + databaseURL: 'https://project-kinsan.firebaseio.com', + }, + firebase = window.firebase; + + if (firebase.apps.length === 0) { + firebase.initializeApp(config); + } + + var database = firebase.database(); + var gameChat = { + + sessionStoreRef : 'game-sessions', + membersRef : 'members', + messagesRef: 'messages', + isTypingRef : 'isTyping', + + sessionExists : function(gameId){ + var that = this; + return new Promise(function(fufill, fail){ + database.ref(that.sessionStoreRef).once('value') + .then(function(snapshot){ + if (snapshot.val() == null) { + fail('Session "'+gameId+'" does not exist'); + } + var exists = (snapshot.val()[gameId] === true); + fufill(exists); + }); + }); + }, + + startSession: function (gameId, members) { + var that = this; + return new Promise(function (fufill, fail) { + //save the gameId + var session = {}; + session[gameId] = true; + database.ref(that.sessionStoreRef).update(session).then(function () { + //save the members for the session + database.ref(that.membersRef + '/' + gameId).set({}) + .then(function () { + var member; + for (member in members) { + if (members.hasOwnProperty(member)) { + database.ref(that.membersRef + '/' + gameId).push(member); + } + } + database.ref(that.messagesRef + '/' + gameId).set({}); + fufill(true); + }).catch(function(error){ + fail(error); + }); + }).catch(function(error){ + fail(error); + }); + }); + }, + + sendMessage: function (msg, gameId, sender) { + var that = this, timestamp = Date.parse(new Date()) / 1000; + return new Promise(function(fufill, fail){ + database.ref(that.messagesRef + '/'+ gameId).push({ + 'sender': sender.name, + 'avatar': sender.avatar, + 'message': msg, + 'timestamp': timestamp + }).then(function () { + fufill(true); + }) + .catch(function(error){ + fail(error); + }); + }); + }, + + loadMessages: function(gameId) { + var that = this; + return new Promise(function(fufill, fail){ + database.ref(that.messagesRef + '/' + gameId).once('value', + function(messages){ + fufill(messages.val()); + }).catch (function(error) { + fail(error); + }); + }); + }, + + listenForMessage : function(gameId,listener){ + database.ref(this.messagesRef + '/' + gameId).on('value', + function (new_message) { + if (typeof listener === 'function') { + listener(new_message.val()); + } + } + ); + }, + + triggerIsTyping : function(gameId, typist){ + var that = this; + return new Promise(function(fulfill, fail){ + database.ref(that.isTypingRef + '/' + gameId).set(typist) + .then(function(){ + fulfill(true); + }).catch(function(error){ + fail(error); + }); + }).catch(function(error){ + fail(error); + }); + }, + + listenForTyping : function(gameId, listener) { + database.ref(this.isTypingRef + '/' + gameId).on('value', + function (typist) { + listener(typist.val()); + }); + } + }; + return gameChat; + }); \ No newline at end of file diff --git a/public/views/game.tpl.html b/public/views/game.tpl.html new file mode 100644 index 0000000..ce5b683 --- /dev/null +++ b/public/views/game.tpl.html @@ -0,0 +1,26 @@ +
+
+
+
+
+ CHAT +
+
+
    +
  • + +
    +

    {{newMessage.sender}} {{newMessage.timestamp | date:'h:mma'}}

    +

    {{newMessage.message}}

    +
    +
  • +
+
+

+
+ + + +
+
+
diff --git a/test/app/game-chat-ctrl.test.js b/test/app/game-chat-ctrl.test.js new file mode 100644 index 0000000..01ba5f7 --- /dev/null +++ b/test/app/game-chat-ctrl.test.js @@ -0,0 +1,59 @@ +'use strict'; +describe('Test for GameChatCtrl', function(){ + var $scope, $controller, GameChat, mockGame; + + beforeEach( + angular.mock.module('mean.gameChat') + ); + + beforeEach(function(){ + inject(function($injector, _$controller_){ + $controller = _$controller_; + GameChat = $injector.get('GameChat'); + }); + }); + + beforeEach(function(){ + $scope = {}; + mockGame = { + id: 789456, // This player's socket ID, so we know who this player is + gameID: '123456789', + players: [], + }; + $controller('GameChatCtrl', {$scope: $scope, game: mockGame}); + + $scope.gameId = 'qwertyuiop'; + $scope.users = [ + {name:'John Doe', avatar:'http://blahblah.com/avatar.png'}, + {name:'Jane Doe',avatar:'http://blahblah.com/gravatar.gif'} + ]; + $scope.currentUser = $scope.users[0]; + }); + + describe('Scope properties check', function(){ + it('should expect property "GameChat" to equal GameChat', function(){ + expect($scope.GameChat).toEqual(GameChat); + }); + + it('should expect property "messages" to be an empty object', function(){ + expect($scope.messages).toEqual({}); + }); + + it('should expect newMessage to be a defined function', function(){ + expect(typeof $scope.newMessage).toEqual('function'); + }); + + it('should expect sendMessage to be a defined function', function(){ + expect(typeof $scope.sendMessage).toEqual('function'); + }); + + it('should expect showIsTyping to be a defined function', function(){ + expect(typeof $scope.showIsTyping).toEqual('function'); + }); + + it('should expect typingListener to be a defined function', function(){ + expect(typeof $scope.typingListener).toEqual('function'); + }); + + }); +}); \ No newline at end of file diff --git a/test/app/game-chat-fac.test.js b/test/app/game-chat-fac.test.js new file mode 100644 index 0000000..8d35b0e --- /dev/null +++ b/test/app/game-chat-fac.test.js @@ -0,0 +1,196 @@ +'use strict'; +describe('Game chat test', function(){ + + beforeEach( + angular.mock.module('mean.gameChat') + ); + + var $factoryProvider, GameChat; + + beforeEach(function(){ + inject(function($injector){ + $factoryProvider = $injector; + }); + GameChat = $factoryProvider.get('GameChat'); + }); + + describe('Test GameChat factory', function(){ + //creating demo data + var validGameId = 'qwertyuiop', + users = [ + {name:'John Doe', avatar:'http://blahblah.com/avatar.png'}, + {name:'Jane Doe',avatar:'http://blahblah.com/gravatar.gif'} + ]; + + beforeEach(function(){ + /* + * we try to change the key for saving data to firebase after + * confirming they've been defined + * */ + if (typeof GameChat.sessionStoreRef === 'string' + && GameChat.sessionStoreRef.trim() !== '' ) { + GameChat.sessionStoreRef = 'test-game-sessions'; + } + + if (typeof GameChat.membersRef === 'string' + && GameChat.membersRef.trim() !== '') { + GameChat.membersRef = 'test-members'; + } + + if (typeof GameChat.messagesRef === 'string' + && GameChat.messagesRef.trim() !== '') { + GameChat.messagesRef = 'test-messages'; + } + + if(typeof GameChat.isTypingRef === 'string' + && GameChat.isTypingRef.trim() !== '') { + GameChat.isTypingRef = 'test-isTyping'; + } + + }); + + describe('Check for properties', function () { + it('should expect property "sessionStoreRef" to be defined',function () { + expect(GameChat.sessionStoreRef).toBeDefined(); + }); + + it('should expect property "messagesRef" to be defined', function(){ + expect(GameChat.messagesRef).toBeDefined(); + }); + + it('should expect property "membersRef" to be defined', function(){ + expect(GameChat.membersRef).toBeDefined(); + }); + }); + + describe('Game session', function(){ + it('should expect "startSession" to be a defined function', function(){ + expect(typeof GameChat.startSession).toEqual('function'); + }); + + it('should expect "sessionExists" to be a defined function', function(){ + expect(typeof GameChat.sessionExists).toEqual('function'); + }); + + it('should be able to create a game session', function(done){ + GameChat.startSession(validGameId, users).then(function(){ + GameChat.sessionExists(validGameId).then(function(data){ + expect(data).toBe(true); + done(); + }).catch(function(){ + expect(false).toBe(true); + done(); + }); + }); + }); + + + it('should assert that a game session with a gameId exist',function(done){ + var nonExistentGameId = 'asdf1234'; + GameChat.sessionExists(nonExistentGameId).then(function(data){ + expect(data).toBe(false); + GameChat.sessionExists(validGameId).then(function(data){ + expect(data).toBe(true); + done(); + }).catch(function(){ + expect(false).toBe(true); + done(); + }); + }); + }); + }); + + describe('Messages', function(){ + + it('should expect sendMessage to be a defined function', function(){ + expect(GameChat.sendMessage).toBeDefined(); + expect(typeof GameChat.sendMessage).toEqual('function'); + }); + + it('should be able to send a message', function(done){ + GameChat.sendMessage('Hello world', validGameId, users[0]) + .then(function(data){ + expect(data).toBe(true); + done(); + }) + .catch(function(){ + expect(false).toBe(true); + done(); + }); + }); + + it('should expect listenForMessage to be a defined function', function(){ + expect(GameChat.listenForMessage).toBeDefined(); + expect(typeof GameChat.listenForMessage).toEqual('function'); + }); + + it('should detect new data changes to firebase', function(done){ + var listener = { + listen : function () { + } + }; + spyOn(listener, 'listen'); + GameChat.listenForMessage(validGameId, listener.listen); + GameChat.sendMessage('Hey', validGameId, users[1]).then(function () { + expect(listener.listen).toHaveBeenCalled(); + done(); + }).catch(function () { + expect(false).toBe(true); + done(); + }); + }); + + it('should expect loadMessages to be a defined function', function(){ + expect(GameChat.loadMessages).toBeDefined(); + expect(typeof GameChat.loadMessages).toEqual('function'); + }); + + it('should expect loadMessages to retrieve all messages', function(done){ + var sentMessages = ['Hello world', 'Hey']; + GameChat.loadMessages(validGameId).then(function(data){ + expect(Object.keys(data).length).toBe(sentMessages.length); + for(var i in data){ + if (data.hasOwnProperty(i)) { + expect(sentMessages.indexOf(data[i].message)) + .toBeGreaterThanOrEqual(0); + } + } + done(); + }).catch(function(){ + expect(false).toBe(true); + done(); + }); + }); + }); + describe('Typing watcher', function(){ + var listener = { + isTyping : function(){ + } + }; + it('should expect triggerIsTyping to be a defined function', function(){ + expect(typeof GameChat.triggerIsTyping).toEqual('function'); + }); + + it('should expect listenForTyping to be a defined function', function(){ + expect(typeof GameChat.listenForTyping).toEqual('function'); + }); + + it('should expect listener.isTyping to be called', function(done){ + spyOn(listener, 'isTyping'); + GameChat.listenForTyping(validGameId, listener.isTyping); + GameChat.triggerIsTyping(validGameId, {name : users[0].name}) + .then(function(){ + expect(listener.isTyping).toHaveBeenCalled(); + done(); + }).catch(function(){ + expect(false).toBe(true); + done(); + }); + + }); + // + }); + + }); + +}); \ No newline at end of file