ucberkeley/moocchat

View on GitHub
app/assets/javascripts/web_socket.js

Summary

Maintainability
C
1 day
Test Coverage
var web_socket = {
  
  group: null,
  ws: null,
  type: null, //both, vote, welcome
  sendChatMessageButton: null,
  voteButton: null,
  lastHeartbeatMap: null,
  heartbeatTimeMs: 10000,
  heartbeatTimeoutMs: 29000,

  initialize: function(chatGroup,taskid,rails_mode, type) {
    this.type = type;
    if(typeof chatGroup == "number"){
      this.group = [chatGroup];
    } else{
      this.group = makeIntArray(chatGroup.split(','));
    }
    // create websocket
    var scheme= (rails_mode == 'production' ? 'wss://' : 'ws://');
    var uri = scheme + window.document.location.host + "/"+chatGroup+","+ taskid;
    this.ws = new WebSocket(uri);
    this.votes = new Array(this.group.length);
    this.voteButton = $('#vote-button');

    // Create heartbeat map and initialize to current time in case we
    // never receive a heartbeat from a group member
    this.lastHeartbeatMap = new Object();
    var currentTime = new Date().getTime();
    for(var i = 0; i < this.group.length; i++) {
      if (this.group[i] != this.taskid) {
        this.lastHeartbeatMap[this.group[i]] = currentTime;
      }
    }

    if(this.isBoth()){
      this.sendChatMessageButton = $('#send-chat-message');
    } else if(this.isVote()){
      this.voteButton.hide();
    }
    this.sendMessages();
    this.receiveMessages();
    this.taskid = taskid;

    var self = this;
    this.ws.onclose = function(evt) {
      if (!evt.wasClean) {
        console.log("WebSocket connection closed abnormally, onclose: code: " + evt.code + ", reason: " + evt.reason);
      }
    };
    this.ws.onerror = function(evt) {
      console.log("WebSocket onerror: evt.data: " + evt.data);
    };

    this.showWelcomeMessage();
  },

  checkVote: function() {
    if(arrays_equal(this.group, this.votes)){
      submitForm();
    }
  },

  vote: function(taskid) {
    if(!contain(this.votes, taskid) & contain(this.group, taskid)){
      this.votes[this.group.indexOf(taskid)] = taskid;
    }
    this.checkVote();
  },

  disconnect: function(other_taskid) {
    var position = this.group.indexOf(other_taskid);
    console.log("Received disconnect notification for task " + other_taskid + " (position " + position + ")");
    if (position == -1) { // Already removed (defensive check)
      return;
    }
    // Remove departed learner from group and votes
    this.group.splice(position, 1);
    this.votes.splice(position, 1);

    // First member of the remaining group is responsible for sending quit message
    // Small bug: Message may be lost if two quit at exact same time
    if (this.taskid == this.group[0])
    {
      this.ws.send(JSON.stringify({ text: "has disconnected", taskid: other_taskid, type: "message" }));
    }
    // Report disconnection to server (server may receive multiple reports)
    // TODO: error handling
    $.ajax({type: "POST", url: "/tasks/" + other_taskid + "/disconnect"});

    this.checkVote(); // the disconnecting learner may have been the last one who didn't vote, in which case proceed
  },

  showMessage: function(text, style_class) {
    if ($("#chat-system").length) {
      $("#chat-system").append("<blockquote class='moocchat-message " + style_class + "'><p>" + text + "</p></blockquote>");
      $("#chat-system").scrollTop($("#chat-system")[0].scrollHeight); // ensure automatic scroll to bottom of chat window
    }
  },

  receiveMessages: function() {
    var self = this;
    this.ws.onmessage = function(message) {
      var data = JSON.parse(message.data)
      if (data.type == "message" & self.isBoth()) {
        self.showMessage(data.text, data.style_class);
      }
      if (data.type == "end-vote") {
        self.vote(data.taskid);
      }
      if (data.type == "disconnect") { 
        self.disconnect(data.taskid);
      }
      if (data.type == "heartbeat" && data.taskid != self.taskid) {
        var currentTime = new Date().getTime();
        self.lastHeartbeatMap[data.taskid] = currentTime;
      }
    };
  },
  
  sendMessages: function() {
    var self = this;
    if(this.isBoth()){
      this.sendChatMessageButton.click(function(event) {
        event.preventDefault();
        var text   = $("#input-text")[0].value;
        self.ws.send(JSON.stringify({ text: text, taskid: self.taskid, type: "message" }));
        $("#input-text")[0].value = "";
        self.sendLog(self.taskid, "chat", text);
      });
    }
    this.voteButton.click(function(event) {
      self.ws.send(JSON.stringify({ text: "has voted to quit chat", taskid: self.taskid, type: "message" }));
      self.ws.send(JSON.stringify({ text: '', taskid: self.taskid, type: "end-vote" }));
      self.sendLog(self.taskid, "quit_chat", "");
      return false;
    });

    window.setInterval(function(message) {
      self.ws.send(JSON.stringify({ text: '', taskid: self.taskid, type: "heartbeat" }));
      var currentTimeMs = new Date().getTime();
      for(var i = 0; i < self.group.length; i++) {
        other_taskid = self.group[i];
        if (other_taskid != self.taskid) {
          if (self.lastHeartbeatMap[other_taskid] < currentTimeMs - self.heartbeatTimeoutMs) {
            console.log("Task " + other_taskid + " has disconnected (heartbeat not received for " + self.heartbeatTimeoutMs + " ms)");
            self.disconnect(other_taskid);
            break;
          }
        }
      }
    }, this.heartbeatTimeMs);
  },
  
  sendLog: function(taskid, name, value){
    log_data = { name: name, value: value };
    $.ajax({
        type: "POST",
        url: "/tasks/" + taskid + "/log",
        data: log_data,
        success: function(data, textStatus, jqXHR){
            console.log("sucessfully logged " + name);
        },
        error: function (data, textStatus, jqXHR){
            console.log("fail to log " + name);
        }
    });
  },

  isBoth: function(){
    return this.type == "both";
  },

  isVote: function(){
    return this.type == "vote";
  },

  showWelcomeMessage: function(){
    if (this.group.length == 1) {
      this.showMessage("No one is currently available to chat with you. You may use this chatroom to reflect on the question. Click the end button above when done.", "system");
    } else if (this.group.length == 2) {
      var you_learner = '';
      var learner = '';
      for(var i = 0; i < this.group.length; i++) {
          if (this.group[i] == this.taskid) {
            you_learner = i + 1;
          } else  {
            learner = i + 1;
          }
      }
      this.showMessage("You are Student " + you_learner + ". Student " + learner + " is also here.", "system");
    } else if (this.group.length == 3) {
      var you_learner = '';
      var learners = '';
      for(var i = 0; i < this.group.length; i++) {
          if (this.group[i] == this.taskid) {
            you_learner = i + 1;
          } else if (this.group[i] != this.taskid) {
              if (learners == '') {
                learners = i + 1;
              } else {
                learners = learners + " and " + (i + 1);
              }
          }
      }
      this.showMessage("You are Student " + you_learner + ". Students " + learners + " are also here.", "system");
    }
  },

  setup: function() {
    var chats = $('#chat-box');
    var votes = $('#vote-box');
    if (chats.length > 0 && votes.length > 0){
      web_socket.initialize(chats.data('chatgroup'),chats.data('taskid'),chats.data('production'), "both");
    }else if(votes.length > 0){
      web_socket.initialize(votes.data('chatgroup'),votes.data('taskid'),votes.data('production'), "vote");
    } else{
      // No chat-box or vote-box on this page, don't initialize websocket
    }
  }
};
$(web_socket.setup);

function submitForm() {
  // if there's a form generated by @start_form_tag macro, submit it
  if ($('form#_main').length > 0) {
    $('form#_main').submit();
  } 
};

function makeIntArray(stringArray){
  for(var i = 0; i < stringArray.length; i ++){
    stringArray[i] = parseInt(stringArray[i]);
  }
  return stringArray;
}

function contain(array, element){
  return array.indexOf(element) != - 1;
}

function arrays_equal(array1, array2){
  if(array1.length != array2.length){
    return false;
  }
  for(var i = 0; i < array1.length; i ++){
    if(array1[i] != array2[i]){
      return false;
    }
  }
  return true;
}