I would like to know how I can improve this chat application of mine. This has been my side-project for a while.
I have made it to support on desktops, tablets and smartphones.
- GitHub link
- Online Demo (feel free to try it out)
Node.js:
index.js
// load all node modules
var express = require('express');
var app = express();
var server = require('http').Server(app); // create and start express server
server.listen(5000, function(){ // change 5000 to process.env.PORT if you are deploying to Heroku
console.log('The chatroom is now live at port # ' + 5000);
});
var io = require('socket.io')(server);
var mongoose = require('mongoose');
// mongoose.set('debug', true); // enable if necessary
// configure mongoose as recommended in: http://blog.mongolab.com/2014/04/mongodb-driver-mongoose/
var mongooseOptions = { server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, replset: { socketOptions: { keepAlive: 1, connectTimeoutMS : 30000 } } };
var mongodbUri = 'mongodb-connection-string';
mongoose.connect(mongodbUri, mongooseOptions);
var conn = mongoose.connection;
conn.on('error', function() {
console.error('MONGO ERROR!');
console.error.bind(console, 'MongoDB connection error:');
});
conn.once('open', function() {
console.log('MongoDB connection openned');
});
// load MongoDB models
var OnlineChatters = require('./models/onlineChatters');
var ChatLog = require('./models/chatLog');
app.use(express.static(__dirname + '/public')); // all static resources for the browser
// views is directory for all template files
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.get('/', function(request, response) {
response.render('pages/index');
});
app.set('port', (process.env.PORT || 5000));
function saveChatLog(nickname, newMessage){
// save the new ChatLog
var newChatLog = new ChatLog({
timestamp: Date.now(),
nickname: nickname,
message: newMessage
});
newChatLog.save(function(err) {
if (err) throw err;
console.log('newChatLog saved successfully!');
});
}
io.on('connection', function (client) {
client.on('join', function(nickname){
client.nickname = nickname;
console.log(client.nickname + " has joined!");
var newMessage = "<span class='italic'><strong>" + client.nickname + "</strong> has joined!</span>"
client.broadcast.emit("messages", newMessage);
saveChatLog(undefined, newMessage); // undefined because message is system generated
client.broadcast.emit("add chatter", client.nickname);
console.log('Broadcasted to all clients, except the new one.');
// get all the users
OnlineChatters.find({}, function(err, result) {
if (err) throw err;
// object of all the users
console.log('all chatters:', result);
result.forEach(function(obj){
client.emit("add chatter", obj.nickname);
console.log('emitted ' + obj.nickname);
});
});
// save new chatter to MongoDB
var newChatter = new OnlineChatters({ nickname: nickname });
newChatter.save(function(err) {
if (err) throw err;
console.log('newChatter saved successfully!');
});
// load earlier messages
ChatLog.find({}).sort({'timestamp': -1}).limit(5).exec(function(err, messages){
if (err) throw err;
// notify that it is loading
if(messages.length === 1){
client.emit("messages", "<span class='italic'>Loading recent 1 message/log.</span>"); // eliminate the s in message(s)
} else if(messages.length !== 0){
client.emit("messages", "<span class='italic'>Loading recent " + messages.length + " messages/logs.</span>");
}
messages.reverse(); // so that it is in chronological order
messages.forEach(function(message){
if(typeof message.nickname !== 'undefined'){ // if not system generated
client.emit("messages", "<strong>" + message.nickname + ":</strong> " + message.message);
} else {
client.emit("messages", message.message);
}
});
client.emit("messages", "<hr/>"); // end of recent messages
var newMessage = "<span class='italic'><strong>" + client.nickname + "</strong> has joined!</span>";
client.broadcast.emit("messages", newMessage); // let the user know
client.emit("messages", newMessage); // let this user know too
saveChatLog(undefined, newMessage); // undefined because message is system generated
});
});
client.on('messages', function (messages) {
client.broadcast.emit("messages", "<strong>" + client.nickname + ":</strong> " + messages);
// save the new ChatLog
saveChatLog(client.nickname, messages);
});
client.on('disconnect', function(nickname){
console.log('in disconnect: ', nickname);
if(client.nickname !== null && typeof client.nickname !== 'undefined'){
client.broadcast.emit("remove chatter", client.nickname);
var newMessage = "<span class='italic'><strong>" + client.nickname + "</strong> has left.</span>";
client.broadcast.emit("messages", newMessage); // let the user know
saveChatLog(undefined, newMessage); // undefined because message strcture is different, it is system generated, no need to save nickname
// remove from database
OnlineChatters.findOneAndRemove({ nickname: client.nickname }, function(err) {
if (err) throw err;
console.log(client.nickname + ' deleted!');
});
}
});
});
Mongoose model chatLog.js
var mongoose = require('mongoose');
var chatLogSchema = new mongoose.Schema({
timestamp: {
type: String,
required: true,
unique: true
},
nickname: String,
message: String
});
module.exports = mongoose.model('chat-log', chatLogSchema);
Mongoose model onlineChatters.js
var mongoose = require('mongoose');
var onlineChattersSchema = new mongoose.Schema({
nickname: String
});
module.exports = mongoose.model('online-chatters', onlineChattersSchema);
Client-side:
app.js
angular
.module('chatroom', [])
.controller('mainController', ['$scope', '$http', '$window', '$timeout', function($scope, $http, $window, $timeout){
// first, establish the socket connection
var server = io('http://localhost:5000'); // change this if you are hosting on a remote server
// hide chatroom until the nickname is entered
$scope.gotNickname = false;
// on connect
server.on('connect', function(data){
$timeout(function(){ // temporary fix for angular
document.getElementById('nickname').focus();
}, 300);
$scope.nicknameSubmitHandler = function () {
if($scope.nickname){
if($scope.nickname.trim() === ''){
$scope.nickname = '';
console.log('Please enter nickname.');
} else {
console.log('Got nickname: ' + $scope.nickname);
server.emit('join', $scope.nickname);
$scope.nicknames = [ {nickname: $scope.nickname} ];
$scope.gotNickname = true;
$timeout(function(){ // temporary fix for angular
document.getElementById('chatInput').focus();
}, 300);
}
} else {
console.log('Please enter nickname.');
}
};
});
// on new chatter
server.on('add chatter', function(nickname){
console.log('Got add chatter request for ' + nickname);
$scope.nicknames.push({nickname: nickname});
$scope.$apply();
});
// on remove chatter
server.on('remove chatter', function(nickname){
if(nickname !== null && typeof nickname !== 'undefined'){
console.log('Someone left: ' + nickname);
console.log('$scope.nicknames', $scope.nicknames);
for(var i=0, l=$scope.nicknames.length; i<l; i++){
if($scope.nicknames[i].nickname === nickname){
$scope.nicknames.splice(i,1);
$scope.$apply();
break;
}
}
}
});
server.on('messages', function(message) {
if(/null/.test(message) === true || /undefined/.test(message) === true){ // temporary fix
return false;
}
angular.element(document.querySelector('#chatLog')).append("<p>" + message + "</p>");
var chatLogDiv = document.getElementById("chatLog");
chatLogDiv.scrollTop = chatLogDiv.scrollHeight - chatLogDiv.clientHeight;
});
$scope.submitHandler = function($event){
$event.preventDefault();
angular.element(document.querySelector('#chatLog')).append("<p class='italic'><strong>Me: </strong>" + $scope.chatInput + "</p>");
var chatLogDiv = document.getElementById("chatLog");
chatLogDiv.scrollTop = chatLogDiv.scrollHeight;
server.emit("messages", $scope.chatInput);
$scope.chatInput = "";
};
$window.addEventListener("beforeunload", function (event) {
return $window.confirm("Do you really want to close?");
});
}]);
main.css
.col-md-10{
height: 400px;
}
div#chatLog{
overflow-y: auto;
height: 350px;
}
ul#chatters{
height: 100%;
overflow-y: auto;
}
ul {
list-style: none;
}
li:before {
content: "";
font-size: 15px;
padding-right: 5px;
display: inline-block;
width: 10px;
height: 10px;
background-color: #4EC919;
border-radius: 50%;
margin-right: 7px;
}
form#chatForm{
margin-bottom: 0px;
text-align: center;
position: absolute;
bottom: 0px;
width: 100%;
}
input#chatInput{
width: 95%;
height: 10%;
outline: none;
padding: 10px 10px 10px 0px;
}
input[type='text'] { /* fix for iPhone Safari zooming */
font-size: 16px;
}
span.italic{
font-style: italic;
}
li.ng-binding.ng-scope:first-child {
font-style: italic;
}
li.ng-binding.ng-scope:first-child:after {
content: " (me)";
}
.container{
margin-top: 50px;
}
.form-control{
border-color: transparent;
padding-left: 0px;
box-shadow: none;
}
.center-text{
text-align: center;
}
.error{
color: red;
font-style: italic;
}
.modal-header {
border-bottom: none;
}
.form-control:focus {
border-color: transparent;
outline: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
index.ejs
<!DOCTYPE html>
<html>
<head>
<% include ../partials/header.ejs %>
</head>
<body ng-app="chatroom">
<div class="container" ng-controller="mainController">
<div class="row">
<div class="col-md-12" ng-hide="gotNickname">
<form ng-submit="nicknameSubmitHandler($event)">
<input type="text"
class="form-control center-text"
ng-model="nickname"
id="nickname"
placeholder="Enter your nickname"
autocomplete="off"
required
autofocus
>
</form>
</div>
</div>
<div class="row">
<div class="col-md-2" id="chattersContainer" ng-show="gotNickname">
<ul id="chatters">
<li ng-repeat="nickname in nicknames">{{nickname.nickname}}</li>
</ul>
</div>
<div class="col-md-10" ng-show="gotNickname">
<div id="chatLog"></div>
<div id="formContainer">
<form id="chatForm" ng-submit="submitHandler($event)">
<input type="text"
id="chatInput"
class="form-control"
ng-model="chatInput"
placeholder="Enter your message"
autocomplete="off"
required
autofocus
>
</form>
</div>
</div>
</div>
</div>
<% include ../partials/scripts.ejs %>
</body>
</html>
header.ejs
<title>Chat</title>
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="/main.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
scripts.ejs
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<script src="../socket.io/socket.io.js"></script> <!-- /node_modules/socket.io/node_modules/socket.io-client/socket.io.js -->
<script type="text/javascript" src="/app.js"></script>