1
\$\begingroup\$

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.

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>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.