<commit_before>'use strict';

////////////////////////////////////////////////////////////

var el = React.DOM;

var hex = {
  'red': '#e74c3c'
};

var helpers = {};
helpers.isTextValid = function(text) {
  return text.trim().length >= 1 && text.trim().length <= 300;
};
// String (Date JSON) -> String
helpers.formatMessageDate = function(dateJson) {
  var date = new Date(dateJson);
  return _.padLeft(date.getHours().toString(), 2, '0') +
    ':' +
    _.padLeft(date.getMinutes().toString(), 2, '0');
};
helpers.generateUuid = function() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
    return v.toString(16);
  });
};
helpers.slugifyUname = function(uname) {
  var slug = uname
    .trim()
    .toLowerCase()
    .replace(/ /g, '-');

  return slug;
};
helpers.escapeHtml = function(unsafe) {
  return unsafe
    .replace(/&/g, "&")
    .replace(/</g, "<")
    .replace(/>/g, ">")
    .replace(/"/g, """)
    .replace(/'/g, "'");
};
helpers.safeAutolink = function(text) {
  return Autolinker.link(helpers.escapeHtml(text), autolinkerOpts);
};
helpers.toMD5HexColor = function(s) {
  return '#' + md5(s).slice(0, 6);
};
helpers.lightenColor = function(color, percent) {

    var R = parseInt(color.substring(1,3),16);
    var G = parseInt(color.substring(3,5),16);
    var B = parseInt(color.substring(5,7),16);

    R = parseInt(R * (100 + percent) / 100);
    G = parseInt(G * (100 + percent) / 100);
    B = parseInt(B * (100 + percent) / 100);

    R = (R<255)?R:255;
    G = (G<255)?G:255;
    B = (B<255)?B:255;

    var RR = ((R.toString(16).length==1)?"0"+R.toString(16):R.toString(16));
    var GG = ((G.toString(16).length==1)?"0"+G.toString(16):G.toString(16));
    var BB = ((B.toString(16).length==1)?"0"+B.toString(16):B.toString(16));

    return "#"+RR+GG+BB;
};
helpers.shadeBlend = function(p,c0,c1) {
    var n=p<0?p*-1:p,u=Math.round,w=parseInt;
    if(c0.length>7){
        var f=c0.split(","),t=(c1?c1:p<0?"rgb(0,0,0)":"rgb(255,255,255)").split(","),R=w(f[0].slice(4)),G=w(f[1]),B=w(f[2]);
        return "rgb("+(u((w(t[0].slice(4))-R)*n)+R)+","+(u((w(t[1])-G)*n)+G)+","+(u((w(t[2])-B)*n)+B)+")"
    }else{
        var f=w(c0.slice(1),16),t=w((c1?c1:p<0?"#000000":"#FFFFFF").slice(1),16),R1=f>>16,G1=f>>8&0x00FF,B1=f&0x0000FF;
        return "#"+(0x1000000+(u(((t>>16)-R1)*n)+R1)*0x10000+(u(((t>>8&0x00FF)-G1)*n)+G1)*0x100+(u(((t&0x0000FF)-B1)*n)+B1)).toString(16).slice(1)
    }
}

// props:
// - muteList
// - receivedServerPayload
var MuteList = React.createClass({
  componentDidUpdate: function() {
    $("abbr.timeago").timeago();
  },
  render: function() {
    return el.div(
      null,
      'Mute list: ' + _.keys(this.props.muteList).length,
      el.ul(
        null,
        _.pairs(this.props.muteList).map(function(pair) {
          var uname = pair[0];
          var duration = pair[1];
          if (duration) {
            duration = new Date(pair[1]);
          }
          return el.li(
            { key: uname },
            el.a(
              {href: '/users/' + helpers.slugifyUname(uname)},
              uname
            ),
            duration === null ?
              ' - ' :
              el.span({ className: 'text-muted' }, ' - Expires '),
            duration === null ?
              el.span({className: 'text-muted'}, 'Forever') :
              el.abbr(
                {
                  className: 'timeago',
                  title: duration.toISOString()
                },
                duration.toISOString()
              )
          );
        })
      )
    );
  }
});

// props: {
//   userList: { Uname -> User }
//   receivedServerPayload: Bool
// }
var UserList = React.createClass({
  render: function() {
    return el.div(
      null,
      'Users online: ',
      !this.props.receivedServerPayload ?
        '--' :
        _.keys(this.props.userList).length,
      !this.props.receivedServerPayload ?
        '' :
        el.ul(
          null,
          _.values(this.props.userList).map(function(u) {
            return el.li(
              null,
              el.a(
                {
                  href: '/users/' + u.slug
                },
                u.uname
              )
            );
          })
        )
    );
  }
});

var App = React.createClass({
  getInitialState: function() {
    return {
      text: '',
      user: undefined,
      messages: new CBuffer(100),
      // String or undefined
      session_id: $('#session-id').attr('data-session-id'),
      socket: undefined,
      userList: {},
      muteList: {},
      receivedServerPayload: false
    };
  },
  componentWillMount: function() {
    var chat_server_url = $('#chat-server-url').attr('data-chat-server-url');
    this.setState({ socket: io(chat_server_url) });
  },
  componentDidMount: function() {
    var self = this;
    this.state.socket.on('reconnect', function() { console.log('Reconnect'); });
    this.state.socket.on('disconnect', function() { console.log('Disconnect'); });
    this.state.socket.on('user_unmuted', function(uname) {
      delete self.state.muteList[uname];
      self.setState({});
    });
    this.state.socket.on('user_muted', function(uname, expires_at) {
      if (expires_at) {
        expires_at = new Date(expires_at);
      }
      self.state.muteList[uname] = expires_at;
      self.setState({});
    });
    this.state.socket.on('connect', function() {
      console.log('connected');
      self.state.socket.emit('auth', { session_id: self.state.session_id }, function(err, data) {
        if (err) {
          console.log('Error:', err);
          return;
        }
        console.log('server responded to auth with payload:', data);

        var userList = {};
        data.users.forEach(function(u) {
          userList[u.uname] = u;
        });

        // HACK: Mutating state outside of setState
        var messages = new CBuffer(100);
        // Autolink them if user messages
        data.messages.forEach(function(m) {
          if (m.user) {
            m.html = helpers.safeAutolink(m.text);
          }
        });
        messages.push.apply(messages, data.messages);
        //self.state.messages.push.apply(self.state.messages, data.messages);

        self.setState({
          user: data.user,
          messages: messages,
          userList: userList,
          muteList: data.muteList,
          receivedServerPayload: true
        }, self._scrollChat);
      });

      ////////////////////////////////////////////////////////////

      self.state.socket.off('new_message').on('new_message', function(message) {
        // autolinkerOpts available from bbcode.js
        // Only autolink non-system messages
        if (message.user) {
          message.html = helpers.safeAutolink(message.text);
        }
        // Hack
        self.state.messages.push(message);
        self.setState({
          //messages: self.state.messages.concat([message])
        }, function() {
          self._onNewMessage();
        });
      });

      ////////////////////////////////////////////////////////////

      self.state.socket.off('user_joined').on('user_joined', function(user) {
        console.log('[received user_joined]', user.uname);

        var userList2 = _.cloneDeep(self.state.userList);
        userList2[user.uname] = user;

        self.setState({ userList: userList2 });
      });

      ////////////////////////////////////////////////////////////

      self.state.socket.off('user_left').on('user_left', function(user) {
        console.log('[received user_left]', user.uname);

        // Is there a sane way to non-destructively remove a key in JS?
        var userList2 = _.cloneDeep(self.state.userList);
        delete userList2[user.uname];

        self.setState({
          userList: userList2
        });
      });

    });
  },
  _onInputChange: function(e) {
    this.setState({ text: e.target.value });
  },
  _onInputKeyDown: function(e) {
    var ENTER = 13;
    if (e.which === ENTER && this.state.user) {
      this._submitMessage();
    }
  },
  _submitMessage: function() {
    var self = this;
    this.state.socket.emit('new_message', this.state.text, function(errString) {
      if (errString) {
        alert('Error: ' + errString);
        return;
      }
      self.setState({ text:  '' }, function() {
        self.refs.input.getDOMNode().focus();
      });
    });
  },
  // New messages should only force scroll if user is scrolled near the bottom
  // already. This allows users to scroll back to earlier convo without being
  // forced to scroll to bottom when new messages arrive
  _onNewMessage: function() {
    var node = this.refs.chatListRef.getDOMNode();

    // Only scroll if user is within 100 pixels of last message
    var shouldScroll = function() {
      var distanceFromBottom = node.scrollHeight - ($(node).scrollTop() + $(node).innerHeight());
      console.log('DistanceFromBottom:', distanceFromBottom);
      return distanceFromBottom <= 100;
    };

    if (shouldScroll()) {
      this._scrollChat();
    }
  },
  _scrollChat: function() {
    var node = this.refs.chatListRef.getDOMNode();
    $(node).scrollTop(node.scrollHeight);
  },
  render: function() {
    return el.div(
      null,
      el.div(
        {className: 'row'},
        el.div(
          {className: 'col-md-9'},
          el.div(
            {className: 'panel panel-default'},
            // panel-body
            el.div(
              {className: 'panel-body'},
              el.ul(
                {
                  ref: 'chatListRef',
                  className: 'list-unstyled messages',
                  style: {
                    overflowY: 'scroll',
                    height: '300px',
                    wordWrap: 'break-word'
                  }
                },
                this.state.messages.toArray().map(function(m) {
                  return el.li(
                    {
                      key: m.id,
                      tabIndex: 1,
                      className: 'message-item'
                    },
                    el.code(
                      null,
                      helpers.formatMessageDate(m.created_at)
                    ),
                    ' ',
                    m.system ?
                      el.code({clasName: 'message-text'}, m.text) :
                      el.a(
                        {
                          href: '/users/' + m.user.slug,
                          target: '_blank'
                        },
                        el.code(
                          {
                            className: 'message-uname',
                            style: {
                              // Lightened. Up = lighter
                              color: helpers.shadeBlend(0.30, helpers.toMD5HexColor(m.user.uname), '#ffffff')
                              // color: helpers.lightencolor(helpers.tomd5hexcolor(m.user.uname), 25)
                              // color: helpers.lightenColor(helpers.toMD5HexColor(m.user.uname), -25)
                            }
                          },
                          m.user.uname + ':'
                        )
                      ),
                    m.system ?
                      '' :
                      el.span(
                        {
                          className: 'message-text',
                          dangerouslySetInnerHTML: {
                            __html: ' ' + m.html
                          }
                        }
                      )
                  );
                })
              )
            ),
            // panel-footer
            el.div(
              {className: 'panel-footer'},
              el.div(
                null,
                el.div(
                  {className: 'row'},
                  el.div(
                    {className: 'col-md-9'},
                    el.input(
                      {
                        type: 'text',
                        placeholder: 'Click here and begin typing...',
                        className: 'form-control message-input',
                        ref: 'input',
                        value: this.state.text,
                        onChange: this._onInputChange,
                        onKeyDown: this._onInputKeyDown
                      }
                    )
                  ),
                  el.div(
                    {className: 'col-md-3'},
                    el.button(
                      {
                        type: 'button',
                        className: 'btn btn-default btn-block',
                        onClick: this._submitMessage,
                        disabled: !helpers.isTextValid(this.state.text) || !this.state.user
                      },
                      this.state.user ? 'Send' : 'Login to chat'
                    )
                  )
                ),
                // Row 2 of footer
                el.div(
                  {className: 'row'},
                  el.div(
                    {className: 'col-md-12'},
                    el.div(
                      {
                        className: 'text-counter' +
                          this.state.text && this.state.text.length > 300 ?
                          ' text-counter-error ' : ''
                      },
                      this.state.text.length + '/300'
                    )
                  )
                )
              )
            )

          )
        ),
        el.div(
          // UserList
          {className: 'col-md-3'},
          React.createElement(UserList, {
            userList: this.state.userList,
            receivedServerPayload: this.state.receivedServerPayload
          }),
          // MuteList
          React.createElement(MuteList, {
            muteList: this.state.muteList,
            receivedServerPayload: this.state.receivedServerPayload
          })
        )
      )
    );
  }
});

React.render(
  React.createElement(App),
  document.getElementById('app')
);
<commit_msg>Allow users to resize chatbox vertically<commit_after>'use strict';

////////////////////////////////////////////////////////////

var el = React.DOM;

var hex = {
  'red': '#e74c3c'
};

var helpers = {};
helpers.isTextValid = function(text) {
  return text.trim().length >= 1 && text.trim().length <= 300;
};
// String (Date JSON) -> String
helpers.formatMessageDate = function(dateJson) {
  var date = new Date(dateJson);
  return _.padLeft(date.getHours().toString(), 2, '0') +
    ':' +
    _.padLeft(date.getMinutes().toString(), 2, '0');
};
helpers.generateUuid = function() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
    return v.toString(16);
  });
};
helpers.slugifyUname = function(uname) {
  var slug = uname
    .trim()
    .toLowerCase()
    .replace(/ /g, '-');

  return slug;
};
helpers.escapeHtml = function(unsafe) {
  return unsafe
    .replace(/&/g, "&")
    .replace(/</g, "<")
    .replace(/>/g, ">")
    .replace(/"/g, """)
    .replace(/'/g, "'");
};
helpers.safeAutolink = function(text) {
  return Autolinker.link(helpers.escapeHtml(text), autolinkerOpts);
};
helpers.toMD5HexColor = function(s) {
  return '#' + md5(s).slice(0, 6);
};
helpers.lightenColor = function(color, percent) {

    var R = parseInt(color.substring(1,3),16);
    var G = parseInt(color.substring(3,5),16);
    var B = parseInt(color.substring(5,7),16);

    R = parseInt(R * (100 + percent) / 100);
    G = parseInt(G * (100 + percent) / 100);
    B = parseInt(B * (100 + percent) / 100);

    R = (R<255)?R:255;
    G = (G<255)?G:255;
    B = (B<255)?B:255;

    var RR = ((R.toString(16).length==1)?"0"+R.toString(16):R.toString(16));
    var GG = ((G.toString(16).length==1)?"0"+G.toString(16):G.toString(16));
    var BB = ((B.toString(16).length==1)?"0"+B.toString(16):B.toString(16));

    return "#"+RR+GG+BB;
};
helpers.shadeBlend = function(p,c0,c1) {
    var n=p<0?p*-1:p,u=Math.round,w=parseInt;
    if(c0.length>7){
        var f=c0.split(","),t=(c1?c1:p<0?"rgb(0,0,0)":"rgb(255,255,255)").split(","),R=w(f[0].slice(4)),G=w(f[1]),B=w(f[2]);
        return "rgb("+(u((w(t[0].slice(4))-R)*n)+R)+","+(u((w(t[1])-G)*n)+G)+","+(u((w(t[2])-B)*n)+B)+")"
    }else{
        var f=w(c0.slice(1),16),t=w((c1?c1:p<0?"#000000":"#FFFFFF").slice(1),16),R1=f>>16,G1=f>>8&0x00FF,B1=f&0x0000FF;
        return "#"+(0x1000000+(u(((t>>16)-R1)*n)+R1)*0x10000+(u(((t>>8&0x00FF)-G1)*n)+G1)*0x100+(u(((t&0x0000FF)-B1)*n)+B1)).toString(16).slice(1)
    }
}

// props:
// - muteList
// - receivedServerPayload
var MuteList = React.createClass({
  componentDidUpdate: function() {
    $("abbr.timeago").timeago();
  },
  render: function() {
    return el.div(
      null,
      'Mute list: ' + _.keys(this.props.muteList).length,
      el.ul(
        null,
        _.pairs(this.props.muteList).map(function(pair) {
          var uname = pair[0];
          var duration = pair[1];
          if (duration) {
            duration = new Date(pair[1]);
          }
          return el.li(
            { key: uname },
            el.a(
              {href: '/users/' + helpers.slugifyUname(uname)},
              uname
            ),
            duration === null ?
              ' - ' :
              el.span({ className: 'text-muted' }, ' - Expires '),
            duration === null ?
              el.span({className: 'text-muted'}, 'Forever') :
              el.abbr(
                {
                  className: 'timeago',
                  title: duration.toISOString()
                },
                duration.toISOString()
              )
          );
        })
      )
    );
  }
});

// props: {
//   userList: { Uname -> User }
//   receivedServerPayload: Bool
// }
var UserList = React.createClass({
  render: function() {
    return el.div(
      null,
      'Users online: ',
      !this.props.receivedServerPayload ?
        '--' :
        _.keys(this.props.userList).length,
      !this.props.receivedServerPayload ?
        '' :
        el.ul(
          null,
          _.values(this.props.userList).map(function(u) {
            return el.li(
              null,
              el.a(
                {
                  href: '/users/' + u.slug
                },
                u.uname
              )
            );
          })
        )
    );
  }
});

var App = React.createClass({
  getInitialState: function() {
    return {
      text: '',
      user: undefined,
      messages: new CBuffer(100),
      // String or undefined
      session_id: $('#session-id').attr('data-session-id'),
      socket: undefined,
      userList: {},
      muteList: {},
      receivedServerPayload: false
    };
  },
  componentWillMount: function() {
    var chat_server_url = $('#chat-server-url').attr('data-chat-server-url');
    this.setState({ socket: io(chat_server_url) });
  },
  componentDidMount: function() {
    var self = this;
    this.state.socket.on('reconnect', function() { console.log('Reconnect'); });
    this.state.socket.on('disconnect', function() { console.log('Disconnect'); });
    this.state.socket.on('user_unmuted', function(uname) {
      delete self.state.muteList[uname];
      self.setState({});
    });
    this.state.socket.on('user_muted', function(uname, expires_at) {
      if (expires_at) {
        expires_at = new Date(expires_at);
      }
      self.state.muteList[uname] = expires_at;
      self.setState({});
    });
    this.state.socket.on('connect', function() {
      console.log('connected');
      self.state.socket.emit('auth', { session_id: self.state.session_id }, function(err, data) {
        if (err) {
          console.log('Error:', err);
          return;
        }
        console.log('server responded to auth with payload:', data);

        var userList = {};
        data.users.forEach(function(u) {
          userList[u.uname] = u;
        });

        // HACK: Mutating state outside of setState
        var messages = new CBuffer(100);
        // Autolink them if user messages
        data.messages.forEach(function(m) {
          if (m.user) {
            m.html = helpers.safeAutolink(m.text);
          }
        });
        messages.push.apply(messages, data.messages);
        //self.state.messages.push.apply(self.state.messages, data.messages);

        self.setState({
          user: data.user,
          messages: messages,
          userList: userList,
          muteList: data.muteList,
          receivedServerPayload: true
        }, self._scrollChat);
      });

      ////////////////////////////////////////////////////////////

      self.state.socket.off('new_message').on('new_message', function(message) {
        // autolinkerOpts available from bbcode.js
        // Only autolink non-system messages
        if (message.user) {
          message.html = helpers.safeAutolink(message.text);
        }
        // Hack
        self.state.messages.push(message);
        self.setState({
          //messages: self.state.messages.concat([message])
        }, function() {
          self._onNewMessage();
        });
      });

      ////////////////////////////////////////////////////////////

      self.state.socket.off('user_joined').on('user_joined', function(user) {
        console.log('[received user_joined]', user.uname);

        var userList2 = _.cloneDeep(self.state.userList);
        userList2[user.uname] = user;

        self.setState({ userList: userList2 });
      });

      ////////////////////////////////////////////////////////////

      self.state.socket.off('user_left').on('user_left', function(user) {
        console.log('[received user_left]', user.uname);

        // Is there a sane way to non-destructively remove a key in JS?
        var userList2 = _.cloneDeep(self.state.userList);
        delete userList2[user.uname];

        self.setState({
          userList: userList2
        });
      });

    });
  },
  _onInputChange: function(e) {
    this.setState({ text: e.target.value });
  },
  _onInputKeyDown: function(e) {
    var ENTER = 13;
    if (e.which === ENTER && this.state.user) {
      this._submitMessage();
    }
  },
  _submitMessage: function() {
    var self = this;
    this.state.socket.emit('new_message', this.state.text, function(errString) {
      if (errString) {
        alert('Error: ' + errString);
        return;
      }
      self.setState({ text:  '' }, function() {
        self.refs.input.getDOMNode().focus();
      });
    });
  },
  // New messages should only force scroll if user is scrolled near the bottom
  // already. This allows users to scroll back to earlier convo without being
  // forced to scroll to bottom when new messages arrive
  _onNewMessage: function() {
    var node = this.refs.chatListRef.getDOMNode();

    // Only scroll if user is within 100 pixels of last message
    var shouldScroll = function() {
      var distanceFromBottom = node.scrollHeight - ($(node).scrollTop() + $(node).innerHeight());
      console.log('DistanceFromBottom:', distanceFromBottom);
      return distanceFromBottom <= 100;
    };

    if (shouldScroll()) {
      this._scrollChat();
    }
  },
  _scrollChat: function() {
    var node = this.refs.chatListRef.getDOMNode();
    $(node).scrollTop(node.scrollHeight);
  },
  render: function() {
    return el.div(
      null,
      el.div(
        {className: 'row'},
        el.div(
          {className: 'col-md-9'},
          el.div(
            {className: 'panel panel-default'},
            // panel-body
            el.div(
              {className: 'panel-body'},
              el.ul(
                {
                  ref: 'chatListRef',
                  className: 'list-unstyled messages',
                  style: {
                    resize: 'vertical',
                    overflowY: 'scroll',
                    height: '300px',
                    wordWrap: 'break-word'
                  }
                },
                this.state.messages.toArray().map(function(m) {
                  return el.li(
                    {
                      key: m.id,
                      tabIndex: 1,
                      className: 'message-item'
                    },
                    el.code(
                      null,
                      helpers.formatMessageDate(m.created_at)
                    ),
                    ' ',
                    m.system ?
                      el.code({clasName: 'message-text'}, m.text) :
                      el.a(
                        {
                          href: '/users/' + m.user.slug,
                          target: '_blank'
                        },
                        el.code(
                          {
                            className: 'message-uname',
                            style: {
                              // Lightened. Up = lighter
                              color: helpers.shadeBlend(0.30, helpers.toMD5HexColor(m.user.uname), '#ffffff')
                              // color: helpers.lightencolor(helpers.tomd5hexcolor(m.user.uname), 25)
                              // color: helpers.lightenColor(helpers.toMD5HexColor(m.user.uname), -25)
                            }
                          },
                          m.user.uname + ':'
                        )
                      ),
                    m.system ?
                      '' :
                      el.span(
                        {
                          className: 'message-text',
                          dangerouslySetInnerHTML: {
                            __html: ' ' + m.html
                          }
                        }
                      )
                  );
                })
              )
            ),
            // panel-footer
            el.div(
              {className: 'panel-footer'},
              el.div(
                null,
                el.div(
                  {className: 'row'},
                  el.div(
                    {className: 'col-md-9'},
                    el.input(
                      {
                        type: 'text',
                        placeholder: 'Click here and begin typing...',
                        className: 'form-control message-input',
                        ref: 'input',
                        value: this.state.text,
                        onChange: this._onInputChange,
                        onKeyDown: this._onInputKeyDown
                      }
                    )
                  ),
                  el.div(
                    {className: 'col-md-3'},
                    el.button(
                      {
                        type: 'button',
                        className: 'btn btn-default btn-block',
                        onClick: this._submitMessage,
                        disabled: !helpers.isTextValid(this.state.text) || !this.state.user
                      },
                      this.state.user ? 'Send' : 'Login to chat'
                    )
                  )
                ),
                // Row 2 of footer
                el.div(
                  {className: 'row'},
                  el.div(
                    {className: 'col-md-12'},
                    el.div(
                      {
                        className: 'text-counter' +
                          this.state.text && this.state.text.length > 300 ?
                          ' text-counter-error ' : ''
                      },
                      this.state.text.length + '/300'
                    )
                  )
                )
              )
            )

          )
        ),
        el.div(
          // UserList
          {className: 'col-md-3'},
          React.createElement(UserList, {
            userList: this.state.userList,
            receivedServerPayload: this.state.receivedServerPayload
          }),
          // MuteList
          React.createElement(MuteList, {
            muteList: this.state.muteList,
            receivedServerPayload: this.state.receivedServerPayload
          })
        )
      )
    );
  }
});

React.render(
  React.createElement(App),
  document.getElementById('app')
);
<|endoftext|>


<commit_before>
/////////////////////////////////////////////////////////
// Chat

/**
 * Firebase chat function handler
 * @constructor
 * @param {object} [opts] - chat options
 * @param {string} [opts.firebaseUrl] - firebase URL of the chat
 */
function Chat(opts) {
	// Define options as an empty object if opts is null
	var options = opts || {};

	// Default configuration
	this.config = {
		firebaseUrl: "https://chatly.firebaseio.com"
	};

	// Set config from options
	for (var item in options) {
        if (options.hasOwnProperty(item)) {
		    this.config[item] = options[item];
        }
	}

	// Contact array
	this.contacts = {};

    // Users in the room
    this.roomUsers = {};

	// User room array
	this.userRooms = {};

	// Job queue
	this.queue = [];

	// Firebase connection reference
	this.connectedRef = new Firebase(this.config.firebaseUrl + "/.info/connected");

	// Set callbacks
	this.on('gotToken', this.handleGotToken.bind(this));
}

// Add event functionality to Chat
asEvented.call(Chat.prototype);

/////////////////////////////////////////////////////////
// Getters

/**
 * Get the name of the 'oneContactRoom' with this user
 * @param {string} username - name of the other user
 * @returns {string} The room name
 */
Chat.prototype.getRoomNameWith = function(username) {
	var roomName;
	if (this.username < username) {
		roomName = this.username + '&' + username;
	} else {
		roomName = username + '&' + this.username;
	}
	return roomName;
};

/**
 * Get the current list of contacts
 * @returns {{?: Contact}} A list of contacts
 */
Chat.prototype.getContacts = function() {
	return this.contacts;
};

/////////////////////////////////////////////////////////
// Chat control

/**
 * Start the chat engine
 */
Chat.prototype.start = function() {
	this.getUserToken();
};

/**
 * Get the user token
 * @fires Chat#event:gotToken
 */
Chat.prototype.getUserToken = function() {
	var self = this;
	$.ajax("/user/gettoken.php")
		.done(function (response) {
			if (response.username && response.token) {
				self.trigger("gotToken", response);
			}
		})
		.fail(function() {
			alert("Error: unable to get your username.");
		});
};

/**
 * Connect to the Firebase chat.
 * Requires Chat#getUserToken to be called before this.
 */
Chat.prototype.connect = function() {
	var self = this;

	this.userRef = new Firebase(this.config.firebaseUrl + "/users/"
		+ this.username);

	this.userRef.auth(this.token, function (error, result) {
		if (error) {
			console.error('Error: unable to login: ' + error);
		} else {
			console.log('Expires at: ' + new Date(result.expires * 1000));
			self.authenticated = true;
			self.handleAuthSuccess.bind(self).call();
		}
	});
};

/////////////////////////////////////////////////////////
// Job queue

/**
 * Run all of the queued jobs.
 * This is used to queue all of the jobs that require the chat to be connected,
 * but it is not yet connected.
 */
Chat.prototype.runQueuedJobs = function() {
	for (var i = this.queue.length - 1; i >= 0; i--) {
		switch (this.queue[i].type) {
			case 'room':
				this.joinRoom(this.queue[i].roomId);
				break;
			case 'contactRoom':
				this.joinContactRoom(this.queue[i].username);
				break;
		}

		delete this.queue[i];
	}
};

/////////////////////////////////////////////////////////
// Contact management methods

/**
 * Send a friend request
 * @param {string} username - The username of the contact to send the request to
 * @param success - the success callback
 * @param failure - the failure callback (with an error message)
 */
Chat.prototype.sendFriendRequest = function(username, success, failure) {
    var self = this;

	var handleResponse = function (data) {
		if (data.success === true) {
			self.handleSendFriendRequest(username);

			if (success) {
				success();
			}
		} else {
			if (failure) {
				failure(data.error);
			}
		}
	}

	$.post(
		"/ajax/check_contact_username.php",
		{username: username},
		handleResponse
	);
};

/**
 * Confirm a friend request
 * @param {string} username - the username of the contact to confirm the friend request from
 */
Chat.prototype.confirmFriendRequest = function(username) {
	var roomName = this.getRoomNameWith(username);

	var roomData = {
		type: 'oneContactRoom',
		users: {}
	};
	roomData.users[this.username] = true;
	roomData.users[username]      = true;

	// Create the user room
	new Firebase(this.config.firebaseUrl + "/rooms/" + roomName)
		.set(roomData);

	// Update contact states
	this.userRef
		.child('contacts')
		.child(username)
		.transaction(function (current_value) {
			if (current_value === false) {
				return true;
			} else {
				return current_value;
			}
		})
	;

	new Firebase(this.config.firebaseUrl + "/users/" + username + "/contacts/" + this.username)
		.transaction(function (current_value) {
			if (current_value === "sent") {
				return true;
			} else {
				return current_value;
			}
		})
	;
};

/**
 * Remove a contact from the contact list
 * @param {string} username - the username of the contact to remove
 */
Chat.prototype.removeContact = function(username) {
	this.userRef
		.child('contacts')
		.child(username)
		.set(null)
	;

	new Firebase(this.config.firebaseUrl + "/users/" + username + "/contacts/" + this.username)
		.set(null)
	;
};

/////////////////////////////////////////////////////////
// Room management methods

/**
 * Join a chat room
 * @param {string} roomId - id of the room to join
 */
Chat.prototype.joinRoom = function(roomId) {
	var self = this;

	// Leave current room
	this.leaveCurrentRoom();

	if (this.authenticated) {
		// Join a new room
		this.roomRef = new Firebase(this.config.firebaseUrl + "/rooms/" + roomId);
		this.roomRef.child('messages').limit(100).on('child_added', function (snapshot) {
			self.trigger('gotChatMessage', snapshot.val());
		});

        this.roomRef.child('users').on('child_added',
            this.handleRoomContactAdded.bind(this));
        this.roomRef.child('users').on('child_removed',
            this.handleRoomContactRemoved.bind(this));

        this.roomRef.child('type').on('value', function (snapshot) {
        	self.trigger('joinedRoom', roomId, snapshot.val());
        });
	} else {
		// Not connected to Firebase yet
		// Add this job to the queue
		this.queue.push({
			type: 'room',
			roomId: roomId
		});
	}
};

/**
 * Join a one-contact room
 * @param {string} username - the username of the contact to join a room with
 */
Chat.prototype.joinContactRoom = function(username) {
	if (this.authenticated) {
		this.joinRoom(this.getRoomNameWith(username));
	} else {
		// Not connected to Firebase yet
		// Add this job to the queue
		this.queue.push({
			type: 'contactRoom',
			username: username
		});
	}
};

/**
 * Create a new multiple-contact room
 * @param {string} name - the name of the new room
 */
Chat.prototype.createRoom = function(name) {
	var roomData = {
		name: name,
		type: 'multipleContactRoom',
		users: {}
	};
	roomData.users[this.username] = true;

	var newRoomRef = new Firebase(this.config.firebaseUrl + "/rooms").push();
	newRoomRef.set(roomData);

	this.userRef.child('rooms').child(newRoomRef.name()).set(true);
};

/**
 * Send a message to the current room
 * @param {string} message
 */
Chat.prototype.sendToRoom = function(message) {
	if (this.roomRef) {
		this.roomRef.child('messages').push().setWithPriority({
			data: message,
			from: this.username,
			time: Firebase.ServerValue.TIMESTAMP
		}, Firebase.ServerValue.TIMESTAMP);
	} else {
		console.warn('Tried to send message when no room is joined');
	}
};

/**
 * Leave the current room
 * @fires Chatly#event:leftRoom
 */
Chat.prototype.leaveCurrentRoom = function() {
	if (this.roomRef) {
		this.roomRef.child('messages').off();
        this.roomRef.child('contacts').off();
		this.roomRef.off();
		this.roomRef = null;
	}

    for (var username in this.roomUsers) {
        if (this.roomUsers.hasOwnProperty(username)) {
            this.roomUsers[username].stop();
            delete this.roomUsers[username];
        }
    }

	this.trigger('leftRoom');
};

/////////////////////////////////////////////////////////
// Room contact methods

Chat.prototype.addContactToRoom = function(username) {
	this.roomRef
		.child('users').child(username)
		.set(true);

	new Firebase(this.config.firebaseUrl + "/users/" + username + "/rooms/" +
		this.roomRef.name()).set(true);
};

/////////////////////////////////////////////////////////
// Chat event handlers

/**
 * Handle authentication success.
 * Subscribes to contact add/remove/change events, room add/remove/change events and
 * changes the current user's login state to 'true'.
 */
Chat.prototype.handleAuthSuccess = function() {
	var self = this;

	this.connectedRef.on('value', function(snapshot) {
		if (snapshot.val() === true) {
			console.log("Connected to Firebase...");

			// On disconnect
			self.userRef.child('loggedIn').onDisconnect().set(null);

			// Set login state
			self.userRef.child('loggedIn').set(true);

			// Contact handlers
			self.userRef.child('contacts').on('child_added',
				self.handleContactAdded.bind(self));
			self.userRef.child('contacts').on('child_removed',
				self.handleContactRemoved.bind(self));

			// Room handlers
			self.userRef.child('rooms').on('child_added',
				self.handleRoomAdded.bind(self));
			self.userRef.child('rooms').on('child_removed',
				self.handleRoomRemoved.bind(self));

			// Do jobs in queue
			self.runQueuedJobs.bind(self).call();
		}
	});
};

/**
 * Handle the 'gotToken' event
 * @param {object} response - the parsed response from the server
 */
Chat.prototype.handleGotToken = function(response) {
	this.username = response.username;
	this.token = response.token;

	this.connect();
};

/**
 * Handle a send friend request event
 * @param {string} username - the name of the user to send the friend request to
 */
Chat.prototype.handleSendFriendRequest = function(username) {
    this.userRef
        .child('contacts')
        .child(username)
        .transaction(function (current_value) {
            if (current_value === null || current_value === undefined) {
                return "sent";
            } else {
                return current_value;
            }
        })
    ;

    new Firebase(this.config.firebaseUrl + "/users/" + username + "/contacts/" + this.username)
        .transaction(function (current_value) {
            if (current_value === null  || current_value === undefined) {
                return false;
            } else {
                return current_value;
            }
        })
    ;
};

/////////////////////////////////////////////////////////
// Firebase event handlers

/**
 * Handle a new contact added event
 * @param snapshot - the Firebase snapshot of the new contact
 */
Chat.prototype.handleContactAdded = function(snapshot) {
	var newContact = new Contact({
		parent: this,
		username: snapshot.name(),
		contactRef: snapshot.ref()
	});

	this.contacts[snapshot.name()] = newContact;
	this.trigger('contactAdded', newContact);
};

/**
 * Handle a contact removed event
 * @param snapshot - the Firebase snapshot of the removed contact
 */
Chat.prototype.handleContactRemoved = function(snapshot) {
	var contact = this.contacts[snapshot.name()];
	delete this.contacts[snapshot.name()];
	contact.stop();
	contact.trigger('removed');
};

/**
 * Handle a new room contact added event
 * @param snapshot - the Firebase snapshot of the new contact
 */
Chat.prototype.handleRoomContactAdded = function(snapshot) {
    if (snapshot.name() !== this.username) {
        var newContact = new Contact({
            parent: this,
            username: snapshot.name(),
            contactRef: snapshot.ref()
        });

        this.roomUsers[snapshot.name()] = newContact;
        this.trigger('roomContactAdded', newContact);
    }
};

/**
 * Handle a room contact removed event
 * @param snapshot - the Firebase snapshot of the removed contact
 */
Chat.prototype.handleRoomContactRemoved = function(snapshot) {
    var contact = this.roomUsers[snapshot.name()];
    delete this.roomUsers[snapshot.name()];
    contact.stop();
    contact.trigger('removed');
};

/**
 * Handle a new room added event
 * @param snapshot - the Firebase snapshot of the room added
 */
Chat.prototype.handleRoomAdded = function(snapshot) {
	var newUserRoom = new UserRoom({
		parent: this,
		roomId: snapshot.name()
	});

	this.userRooms[snapshot.name()] = newUserRoom;

	this.trigger('roomAdded', newUserRoom);
};

/**
 * Handle a room removed event
 * @param snapshot - the Firebase snapshot of the room removed
 */
Chat.prototype.handleRoomRemoved = function(snapshot) {
	var room = this.userRooms[snapshot.name()];
	delete this.userRooms[snapshot.name()];
	room.stop();
	room.trigger('removed');
};
<commit_msg>Added removeCurrentRoom() method to chat.js<commit_after>
/////////////////////////////////////////////////////////
// Chat

/**
 * Firebase chat function handler
 * @constructor
 * @param {object} [opts] - chat options
 * @param {string} [opts.firebaseUrl] - firebase URL of the chat
 */
function Chat(opts) {
	// Define options as an empty object if opts is null
	var options = opts || {};

	// Default configuration
	this.config = {
		firebaseUrl: "https://chatly.firebaseio.com"
	};

	// Set config from options
	for (var item in options) {
        if (options.hasOwnProperty(item)) {
		    this.config[item] = options[item];
        }
	}

	// Contact array
	this.contacts = {};

    // Users in the room
    this.roomUsers = {};

	// User room array
	this.userRooms = {};

	// Job queue
	this.queue = [];

	// Firebase connection reference
	this.connectedRef = new Firebase(this.config.firebaseUrl + "/.info/connected");

	// Set callbacks
	this.on('gotToken', this.handleGotToken.bind(this));
}

// Add event functionality to Chat
asEvented.call(Chat.prototype);

/////////////////////////////////////////////////////////
// Getters

/**
 * Get the name of the 'oneContactRoom' with this user
 * @param {string} username - name of the other user
 * @returns {string} The room name
 */
Chat.prototype.getRoomNameWith = function(username) {
	var roomName;
	if (this.username < username) {
		roomName = this.username + '&' + username;
	} else {
		roomName = username + '&' + this.username;
	}
	return roomName;
};

/**
 * Get the current list of contacts
 * @returns {{?: Contact}} A list of contacts
 */
Chat.prototype.getContacts = function() {
	return this.contacts;
};

/////////////////////////////////////////////////////////
// Chat control

/**
 * Start the chat engine
 */
Chat.prototype.start = function() {
	this.getUserToken();
};

/**
 * Get the user token
 * @fires Chat#event:gotToken
 */
Chat.prototype.getUserToken = function() {
	var self = this;
	$.ajax("/user/gettoken.php")
		.done(function (response) {
			if (response.username && response.token) {
				self.trigger("gotToken", response);
			}
		})
		.fail(function() {
			alert("Error: unable to get your username.");
		});
};

/**
 * Connect to the Firebase chat.
 * Requires Chat#getUserToken to be called before this.
 */
Chat.prototype.connect = function() {
	var self = this;

	this.userRef = new Firebase(this.config.firebaseUrl + "/users/"
		+ this.username);

	this.userRef.auth(this.token, function (error, result) {
		if (error) {
			console.error('Error: unable to login: ' + error);
		} else {
			console.log('Expires at: ' + new Date(result.expires * 1000));
			self.authenticated = true;
			self.handleAuthSuccess.bind(self).call();
		}
	});
};

/////////////////////////////////////////////////////////
// Job queue

/**
 * Run all of the queued jobs.
 * This is used to queue all of the jobs that require the chat to be connected,
 * but it is not yet connected.
 */
Chat.prototype.runQueuedJobs = function() {
	for (var i = this.queue.length - 1; i >= 0; i--) {
		switch (this.queue[i].type) {
			case 'room':
				this.joinRoom(this.queue[i].roomId);
				break;
			case 'contactRoom':
				this.joinContactRoom(this.queue[i].username);
				break;
		}

		delete this.queue[i];
	}
};

/////////////////////////////////////////////////////////
// Contact management methods

/**
 * Send a friend request
 * @param {string} username - The username of the contact to send the request to
 * @param success - the success callback
 * @param failure - the failure callback (with an error message)
 */
Chat.prototype.sendFriendRequest = function(username, success, failure) {
    var self = this;

	var handleResponse = function (data) {
		if (data.success === true) {
			self.handleSendFriendRequest(username);

			if (success) {
				success();
			}
		} else {
			if (failure) {
				failure(data.error);
			}
		}
	}

	$.post(
		"/ajax/check_contact_username.php",
		{username: username},
		handleResponse
	);
};

/**
 * Confirm a friend request
 * @param {string} username - the username of the contact to confirm the friend request from
 */
Chat.prototype.confirmFriendRequest = function(username) {
	var roomName = this.getRoomNameWith(username);

	var roomData = {
		type: 'oneContactRoom',
		users: {}
	};
	roomData.users[this.username] = true;
	roomData.users[username]      = true;

	// Create the user room
	new Firebase(this.config.firebaseUrl + "/rooms/" + roomName)
		.set(roomData);

	// Update contact states
	this.userRef
		.child('contacts')
		.child(username)
		.transaction(function (current_value) {
			if (current_value === false) {
				return true;
			} else {
				return current_value;
			}
		})
	;

	new Firebase(this.config.firebaseUrl + "/users/" + username + "/contacts/" + this.username)
		.transaction(function (current_value) {
			if (current_value === "sent") {
				return true;
			} else {
				return current_value;
			}
		})
	;
};

/**
 * Remove a contact from the contact list
 * @param {string} username - the username of the contact to remove
 */
Chat.prototype.removeContact = function(username) {
	this.userRef
		.child('contacts')
		.child(username)
		.set(null)
	;

	new Firebase(this.config.firebaseUrl + "/users/" + username + "/contacts/" + this.username)
		.set(null)
	;
};

/////////////////////////////////////////////////////////
// Room management methods

/**
 * Join a chat room
 * @param {string} roomId - id of the room to join
 */
Chat.prototype.joinRoom = function(roomId) {
	var self = this;

	// Leave current room
	this.leaveCurrentRoom();

	if (this.authenticated) {
		// Join a new room
		this.roomRef = new Firebase(this.config.firebaseUrl + "/rooms/" + roomId);
		this.roomRef.child('messages').limit(100).on('child_added', function (snapshot) {
			self.trigger('gotChatMessage', snapshot.val());
		});

        this.roomRef.child('users').on('child_added',
            this.handleRoomContactAdded.bind(this));
        this.roomRef.child('users').on('child_removed',
            this.handleRoomContactRemoved.bind(this));

        this.roomRef.child('type').on('value', function (snapshot) {
        	self.trigger('joinedRoom', roomId, snapshot.val());
        });
	} else {
		// Not connected to Firebase yet
		// Add this job to the queue
		this.queue.push({
			type: 'room',
			roomId: roomId
		});
	}
};

/**
 * Join a one-contact room
 * @param {string} username - the username of the contact to join a room with
 */
Chat.prototype.joinContactRoom = function(username) {
	if (this.authenticated) {
		this.joinRoom(this.getRoomNameWith(username));
	} else {
		// Not connected to Firebase yet
		// Add this job to the queue
		this.queue.push({
			type: 'contactRoom',
			username: username
		});
	}
};

/**
 * Create a new multiple-contact room
 * @param {string} name - the name of the new room
 */
Chat.prototype.createRoom = function(name) {
	var roomData = {
		name: name,
		type: 'multipleContactRoom',
		users: {}
	};
	roomData.users[this.username] = true;

	var newRoomRef = new Firebase(this.config.firebaseUrl + "/rooms").push();
	newRoomRef.set(roomData);

	this.userRef.child('rooms').child(newRoomRef.name()).set(true);
};

/**
 * Send a message to the current room
 * @param {string} message
 */
Chat.prototype.sendToRoom = function(message) {
	if (this.roomRef) {
		this.roomRef.child('messages').push().setWithPriority({
			data: message,
			from: this.username,
			time: Firebase.ServerValue.TIMESTAMP
		}, Firebase.ServerValue.TIMESTAMP);
	} else {
		console.warn('Tried to send message when no room is joined');
	}
};

/**
 * Leave the current room
 * @fires Chatly#event:leftRoom
 */
Chat.prototype.leaveCurrentRoom = function() {
	if (this.roomRef) {
		this.roomRef.child('messages').off();
        this.roomRef.child('contacts').off();
		this.roomRef.off();
		this.roomRef = null;
	}

    for (var username in this.roomUsers) {
        if (this.roomUsers.hasOwnProperty(username)) {
            this.roomUsers[username].stop();
            delete this.roomUsers[username];
        }
    }

	this.trigger('leftRoom');
};

/**
 * Leave the current room and remove it from the user's room list.
 */
Chat.prototype.removeCurrentRoom = function() {
	var roomId = this.roomRef.name();

	this.userRef.child('rooms').child(roomId)
		.set(null);
	this.roomRef.child('users').child(this.username)
		.set(null);

	this.leaveCurrentRoom();
};

/////////////////////////////////////////////////////////
// Room contact methods

Chat.prototype.addContactToRoom = function(username) {
	this.roomRef
		.child('users').child(username)
		.set(true);

	new Firebase(this.config.firebaseUrl + "/users/" + username + "/rooms/" +
		this.roomRef.name()).set(true);
};

/////////////////////////////////////////////////////////
// Chat event handlers

/**
 * Handle authentication success.
 * Subscribes to contact add/remove/change events, room add/remove/change events and
 * changes the current user's login state to 'true'.
 */
Chat.prototype.handleAuthSuccess = function() {
	var self = this;

	this.connectedRef.on('value', function(snapshot) {
		if (snapshot.val() === true) {
			console.log("Connected to Firebase...");

			// On disconnect
			self.userRef.child('loggedIn').onDisconnect().set(null);

			// Set login state
			self.userRef.child('loggedIn').set(true);

			// Contact handlers
			self.userRef.child('contacts').on('child_added',
				self.handleContactAdded.bind(self));
			self.userRef.child('contacts').on('child_removed',
				self.handleContactRemoved.bind(self));

			// Room handlers
			self.userRef.child('rooms').on('child_added',
				self.handleRoomAdded.bind(self));
			self.userRef.child('rooms').on('child_removed',
				self.handleRoomRemoved.bind(self));

			// Do jobs in queue
			self.runQueuedJobs.bind(self).call();
		}
	});
};

/**
 * Handle the 'gotToken' event
 * @param {object} response - the parsed response from the server
 */
Chat.prototype.handleGotToken = function(response) {
	this.username = response.username;
	this.token = response.token;

	this.connect();
};

/**
 * Handle a send friend request event
 * @param {string} username - the name of the user to send the friend request to
 */
Chat.prototype.handleSendFriendRequest = function(username) {
    this.userRef
        .child('contacts')
        .child(username)
        .transaction(function (current_value) {
            if (current_value === null || current_value === undefined) {
                return "sent";
            } else {
                return current_value;
            }
        })
    ;

    new Firebase(this.config.firebaseUrl + "/users/" + username + "/contacts/" + this.username)
        .transaction(function (current_value) {
            if (current_value === null  || current_value === undefined) {
                return false;
            } else {
                return current_value;
            }
        })
    ;
};

/////////////////////////////////////////////////////////
// Firebase event handlers

/**
 * Handle a new contact added event
 * @param snapshot - the Firebase snapshot of the new contact
 */
Chat.prototype.handleContactAdded = function(snapshot) {
	var newContact = new Contact({
		parent: this,
		username: snapshot.name(),
		contactRef: snapshot.ref()
	});

	this.contacts[snapshot.name()] = newContact;
	this.trigger('contactAdded', newContact);
};

/**
 * Handle a contact removed event
 * @param snapshot - the Firebase snapshot of the removed contact
 */
Chat.prototype.handleContactRemoved = function(snapshot) {
	var contact = this.contacts[snapshot.name()];
	delete this.contacts[snapshot.name()];
	contact.stop();
	contact.trigger('removed');
};

/**
 * Handle a new room contact added event
 * @param snapshot - the Firebase snapshot of the new contact
 */
Chat.prototype.handleRoomContactAdded = function(snapshot) {
    if (snapshot.name() !== this.username) {
        var newContact = new Contact({
            parent: this,
            username: snapshot.name(),
            contactRef: snapshot.ref()
        });

        this.roomUsers[snapshot.name()] = newContact;
        this.trigger('roomContactAdded', newContact);
    }
};

/**
 * Handle a room contact removed event
 * @param snapshot - the Firebase snapshot of the removed contact
 */
Chat.prototype.handleRoomContactRemoved = function(snapshot) {
    var contact = this.roomUsers[snapshot.name()];
    delete this.roomUsers[snapshot.name()];
    contact.stop();
    contact.trigger('removed');
};

/**
 * Handle a new room added event
 * @param snapshot - the Firebase snapshot of the room added
 */
Chat.prototype.handleRoomAdded = function(snapshot) {
	var newUserRoom = new UserRoom({
		parent: this,
		roomId: snapshot.name()
	});

	this.userRooms[snapshot.name()] = newUserRoom;

	this.trigger('roomAdded', newUserRoom);
};

/**
 * Handle a room removed event
 * @param snapshot - the Firebase snapshot of the room removed
 */
Chat.prototype.handleRoomRemoved = function(snapshot) {
	var room = this.userRooms[snapshot.name()];
	delete this.userRooms[snapshot.name()];
	room.stop();
	room.trigger('removed');
};
<|endoftext|>


<commit_before>'use strict';

var tags = [];

$(document).ready(function() {

    $("#addtag").click(function(e) {
        e.preventDefault();
        var newtag = $("#addtag-text").val();
        if (newtag!="" && $.inArray(newtag, tags)==-1){
            tags.push(newtag);
            $(".tags").append(''+newtag+' ');
            $("#hiddenTags").val($("#hiddenTags").val()+"|"+newtag);
        }
        $("#addtag-text").val("");
    });

    $("#addtag-text").keydown(function(e) {
        if(e.which === 13) {
            $("#addtag").click();
            e.preventDefault();
        }
    });

    $("#entryText").focus(function(e) {
        $("#addtag").click();
    });

    var tagSuggestions = new Bloodhound({
      datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.tag); },
      queryTokenizer: Bloodhound.tokenizers.whitespace,
      limit: 10,
      prefetch: {
        url: '/tag_suggest.json',
        ttl: 0,
        filter: function(list) {
          return $.map(list, function(suggestion) { return { tag: suggestion }; });
        }
      }
    });

    tagSuggestions.initialize();

    $('#addtag-text').typeahead(null, {
      name: 'suggestions',
      displayKey: 'tag',
      source: tagSuggestions.ttAdapter()
    });

    $('#entrySubmitBtn').click(function(e) {
        /* if text present then don't show error message */
        if($("#entryText").val()!="") {
            e.stopImmediatePropagation();
            e.preventDefault();
            /* hacky test to only log click events on new entry */
            if(document.title != 'Edit') {
              var emotion = $("#emotionField").val();
              var emotionValue = (emotion == 'default') ? 'no' : 'yes';
              ga("send", "event", "save", "click", emotionValue, {
                'hitCallback': function() {
                  return true;
                }
              });
            }
        }
    })

})<commit_msg>more fixez<commit_after>'use strict';

var tags = [];

$(document).ready(function() {

    $("#addtag").click(function(e) {
        e.preventDefault();
        var newtag = $("#addtag-text").val();
        if (newtag!="" && $.inArray(newtag, tags)==-1){
            tags.push(newtag);
            $(".tags").append(''+newtag+' ');
            $("#hiddenTags").val($("#hiddenTags").val()+"|"+newtag);
        }
        $("#addtag-text").val("");
    });

    $("#addtag-text").keydown(function(e) {
        if(e.which === 13) {
            $("#addtag").click();
            e.preventDefault();
        }
    });

    $("#entryText").focus(function(e) {
        $("#addtag").click();
    });

    var tagSuggestions = new Bloodhound({
      datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.tag); },
      queryTokenizer: Bloodhound.tokenizers.whitespace,
      limit: 10,
      prefetch: {
        url: '/tag_suggest.json',
        ttl: 0,
        filter: function(list) {
          return $.map(list, function(suggestion) { return { tag: suggestion }; });
        }
      }
    });

    tagSuggestions.initialize();

    $('#addtag-text').typeahead(null, {
      name: 'suggestions',
      displayKey: 'tag',
      source: tagSuggestions.ttAdapter()
    });

    $('#entrySubmitBtn').click(function(e) {
        /* if text present then don't show error message */
        if($("#entryText").val()!="") {
            e.stopImmediatePropagation();
            e.preventDefault();
            /* hacky test to only log click events on new entry */
            if(document.title != 'Edit') {
              var emotion = $("#emotionField").val();
              var emotionValue = (emotion == 'default') ? 'no' : 'yes';
              ga("send", "event", "save", "click", emotionValue, {
                'hitCallback': function() {
                  $(".entry-form").submit();
                }
              });
            }
        }
    })

})<|endoftext|>


<commit_before>$(document).ready(function() {

  function round1() {
    var queryString = $('#query').val();
    var filename = $('#filename').html();
    var T = [];
    for (i=0; i<queryString.length; i++) {
      var bitArray = sjcl.hash.sha256.hash(queryString.substring(0,i+1));
      var formattedString = sjcl.codec.hex.fromBits(bitArray);
      T = T.concat(formattedString);
    }

    console.log(T);
    console.log(queryString);
    console.log(filename);
    console.log('hi');
    $.ajax({
      url: '/file/' + filename + '/query/1',
      type: 'POST',
      data: {
        T: T
      },
      success: function(data) {
        var success = data.success;
        var message = data.message;
        if (success) {
          var found = data.found;
          if (found) {
            console.log('success!');
            console.log(message);
            $('#message').html(message);
            round2(data.encryptedIndex);
          } else {
            $('#message').html(message);
          }
        } else {
          $('#message').html(message);
        }
      },
      error: function(xhr, status, error) {
        console.log('oh noo');
      }
    });
  };

  function round2(encryptedIndex) {
    console.log('round 2 of communication protocol');
    var filename = $('#filename').html();

    K_C = 'This is a key234'
    IV_C = 'This is an IV567'

    var queryString = $('#query').val();
    var length = queryString.length;
    var adata = [];
    var key_bit = sjcl.codec.utf8String.toBits(K_C);
    var iv_bit = sjcl.codec.utf8String.toBits(IV_C);
    var prp = new sjcl.cipher.aes(key_bit);

    var ciphertext_bit = sjcl.codec.base64.toBits(encryptedIndex);
    var decrypted_ciphertext = sjcl.mode.cbc.decrypt(prp, ciphertext_bit, iv_bit, adata);
    var actual_decrypted_ctxt = sjcl.codec.base64.fromBits(decrypted_ciphertext);

    var startIndex = actual_decrypted_ctxt;

    $.ajax({
      url: '/file/' + filename + '/query/2',
      type: 'POST',
      data: {
        startIndex: startIndex,
        length: length
      },
      success: function(data) {
        var success = data.success;
        var message = data.message;
        if (success) {
          var C = data.C;
          console.log('success!');
          console.log(message);
          console.log("C: ", C);
          $('#message').html(message);
          round3();
        } else {
          console.log("no success RIP");
          $('#message').html(message);
        }
      },
      error: function(xhr, status, error) {
        console.log('oh noo');
      }
    });
  };

  function round3() {
    console.log('check whether strings match');
  };

  $('#submit').click(round1);
});
<commit_msg>added cbc mode with ugly code<commit_after>$(document).ready(function() {

  function round1() {
    var queryString = $('#query').val();
    var filename = $('#filename').html();
    var T = [];
    for (i=0; i<queryString.length; i++) {
      var bitArray = sjcl.hash.sha256.hash(queryString.substring(0,i+1));
      var formattedString = sjcl.codec.hex.fromBits(bitArray);
      T = T.concat(formattedString);
    }

    console.log(T);
    console.log(queryString);
    console.log(filename);
    console.log('hi');
    $.ajax({
      url: '/file/' + filename + '/query/1',
      type: 'POST',
      data: {
        T: T
      },
      success: function(data) {
        var success = data.success;
        var message = data.message;
        if (success) {
          var found = data.found;
          if (found) {
            console.log('success!');
            console.log(message);
            $('#message').html(message);
            round2(data.encryptedIndex);
          } else {
            $('#message').html(message);
          }
        } else {
          $('#message').html(message);
        }
      },
      error: function(xhr, status, error) {
        console.log('oh noo');
      }
    });
  };

  function round2(encryptedIndex) {
    console.log('round 2 of communication protocol');
    var filename = $('#filename').html();

    sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."]();

    K_C = 'This is a key234'
    IV_C = 'This is an IV567'

    var queryString = $('#query').val();
    var length = queryString.length;
    var adata = [];
    var key_bit = sjcl.codec.utf8String.toBits(K_C);
    var iv_bit = sjcl.codec.utf8String.toBits(IV_C);
    var prp = new sjcl.cipher.aes(key_bit);

    var ciphertext_bit = sjcl.codec.base64.toBits(encryptedIndex);
    var decrypted_ciphertext = sjcl.mode.cbc.decrypt(prp, ciphertext_bit, iv_bit, adata);
    var actual_decrypted_ctxt = sjcl.codec.base64.fromBits(decrypted_ciphertext);

    var startIndex = actual_decrypted_ctxt;

    $.ajax({
      url: '/file/' + filename + '/query/2',
      type: 'POST',
      data: {
        startIndex: startIndex,
        length: length
      },
      success: function(data) {
        var success = data.success;
        var message = data.message;
        if (success) {
          var C = data.C;
          console.log('success!');
          console.log(message);
          console.log("C: ", C);
          $('#message').html(message);
          round3();
        } else {
          console.log("no success RIP");
          $('#message').html(message);
        }
      },
      error: function(xhr, status, error) {
        console.log('oh noo');
      }
    });
  };

  function round3() {
    console.log('check whether strings match');
  };

  $('#submit').click(round1);
});
<|endoftext|>


<commit_before>$(function() {
	// TODO: Put constants somwhere
	var CARDS_PER_ROW = 3;
	var ANIMATION_TIME = 500;

	var OPEN_SIZE = {
		WIDTH: '100%',
		HEIGHT: '100%'
	};
	var CLOSED_SIZE = {
		WIDTH: '33.3%',
		HEIGHT: '33.3%'
	};
	var HIDE_SIZE = {
		WIDTH: '0%',
		HEIGHT: '0%'
	};

	// Page state variables
	var cardIsOpen = false;
	var $cards = $('.card');
	var numCards = $cards.length;
	var fitTexts = {
		'profile': []
	};

	// On load
	(function () {
		// Populate the fitText fields
		fitTexts['profile'].push(
			$('.profileText .name').fitText(1.3),
			$('.profileText .bio').fitText(3),
			$('.profile .closeButton .content').fitText(0.2)
		);

		// Resizes the profile video on window resize
		$(window).on('resize orientationchange', function () {
			// TODO: Make this happen only if the profile card is open
			resizeProfileVideo();
		});

		// Close button
		$('.closeButton').click(function () {
			closeCard($(this).closest('.card'));
		});

		// If you click outside the card, it automatically closes the opened card
		$('.cardArea').click(function (event) {
			var target = event.target;
			if ($(target).hasClass('cardArea')) {
				closeCard($('.card.open'));
			}
		});
	})();

	//
	// Events
	//

	/**
	 * When clickcing on a card, open it up.
	 */
	$cards.click(function() {
		if (!cardIsOpen) {
			var $card = $(this);
			openCard($card);
		}
	});

	/**
	 * Called during steps of a card resizing
	 * @param {String} cardId The id of the card that is being resized
	 */
	function resizing (cardId) {
		fitTexts[cardId] = fitTexts[cardId] || [];
		fitTexts[cardId].forEach(function (resize) {
			resize();
		});

		// Special resizing cards
		switch (cardId) {
			case 'profile':
				resizeProfileVideo();
				break;
		}
	}

	/**
	 * Resizes the profile video and adjusts the position
	 */
	function resizeProfileVideo() {
		// Resize video
		var $video = $('.profileVideo');
		var videoWidth = $video.width();
		var videoHeight = $video.height();

		var $card = $video.closest('.card');
		var videoAreaHeight = $card.outerHeight();
		var videoAreaWidth = $card.outerWidth() / 3; // TODO: Make this more robust

		var videoRatio = videoHeight/videoWidth;
		var videoAreaRatio = videoAreaHeight/videoAreaWidth;

		var margin;
		// If videoArea too flat, make 100% height
		if (videoRatio < videoAreaHeight/videoAreaWidth) {
			margin = (videoAreaWidth - videoWidth)/2;
			$video.css({
				'width': 'auto',
				'height': '100%',
				'margin-top': 0,
				'margin-left': margin
			});
		} else {
			margin = (videoAreaHeight - videoHeight)/2;
			$video.css({
				'width': '100%',
				'height': 'auto',
				'margin-top': margin,
				'margin-left': 0
			});
		}
	}

	//
	// Actions
	//

	/**
	 * Opens a card. Expands it to fill the view
	 * @param {Card} $card The jquery-wrapped card
	 */
	function openCard ($card) {
		cardIsOpen = true;

		var cardId = $card.data('id');
		var cardIndex = $card.index();
		var siblingCardIndices = getRowIndices(cardIndex);
		var siblingCards = $(getCards(siblingCardIndices).filter(function (card) {
			return card !== $card[0];
		}));

		// Animate the clicked card to open
		$card.animate({
			width: OPEN_SIZE.WIDTH,
			height: OPEN_SIZE.HEIGHT
		}, {
			duration: ANIMATION_TIME,
			step: function () {
				resizing(cardId);
			},
			done: function () {

			}
		});

		// Animate the contents of the clicked card
		$card.find('.closed').show().fadeOut(ANIMATION_TIME);

		// Animate the sibling cards on the same row to closed
		siblingCards.each(function () {
			var $siblingCard = $(this);
			$(this).animate({
				width: HIDE_SIZE.WIDTH,
				height: OPEN_SIZE.HEIGHT
			}, ANIMATION_TIME, function () {
				$siblingCard.hide();
			});
		});

		// Set some properties on the opened card
		$card.addClass('open');
	}

	/**
	 * Closes a card.
	 * @param {Card} $card The jquery-wrapped card
	 */
	function closeCard ($card) {
		var cardId = $card.data('id');
		var cardIndex = $card.index();
		var siblingCardIndices = getRowIndices(cardIndex);
		var siblingCards = $(getCards(siblingCardIndices).filter(function (card) {
			return card !== $card[0];
		}));

		// Animate the clicked card to closed
		$card.animate({
			width: CLOSED_SIZE.WIDTH,
			height: CLOSED_SIZE.HEIGHT
		}, {
			duration: ANIMATION_TIME,
			step: function () {
				resizing(cardId);
			},
			done: function () {
				// Say card is open after all transitions are done
				cardIsOpen = false;
			}
		});

		// Animate the contents of the clicked card
		$card.find('.closed').hide().fadeIn(ANIMATION_TIME);

		// Animate the sibling cards on the same row to closed
		siblingCards.each(function () {
			var $siblingCard = $(this);
			$siblingCard.show();
			$(this).animate({
				width: CLOSED_SIZE.WIDTH,
				height: CLOSED_SIZE.HEIGHT
			}, ANIMATION_TIME);
		});

		$card.removeClass('open');
	}

	/**
	 * Gets an array of the indices of a certain row
	 * @param {Integer} cardIndex An index of a card to get the row from
	 * @returns {Integer[]} An array containing all the card indices of the specified row
	 */
	function getRowIndices(cardIndex) {
		var rowNum = Math.ceil((cardIndex + 1)/CARDS_PER_ROW) - 1;
		var colIndices = new Array(CARDS_PER_ROW);
		for (var i = 0; i < CARDS_PER_ROW; ++i) {
			colIndices[i] = rowNum * CARDS_PER_ROW + i;
		}
		return colIndices;
	}

	/**
	 * Gets the cards of a certain index or indices
	 * @param {Integer|Integer[]} indices The index/indices to get
	 * @returns {Card|Cards[]} The jquery-wrapped Card(s)
	 */
	function getCards (indices) {
		if (typeof indices === 'number') {
			return $cards[indices];
		} else { // Assume it's an array
			return indices.map(function(i) {
				return $cards[i];
			});
		}
	}
});<commit_msg>Fixed height<commit_after>$(function() {
	// TODO: Put constants somwhere
	var CARDS_PER_ROW = 3;
	var ANIMATION_TIME = 500;

	var OPEN_SIZE = {
		WIDTH: '100%',
		HEIGHT: '100%'
	};
	var CLOSED_SIZE = {
		WIDTH: '33.3%',
		HEIGHT: '40%'
	};
	var HIDE_SIZE = {
		WIDTH: '0%',
		HEIGHT: '0%'
	};

	// Page state variables
	var cardIsOpen = false;
	var $cards = $('.card');
	var numCards = $cards.length;
	var fitTexts = {
		'profile': []
	};

	// On load
	(function () {
		// Populate the fitText fields
		fitTexts['profile'].push(
			$('.profileText .name').fitText(1.3),
			$('.profileText .bio').fitText(3),
			$('.profile .closeButton .content').fitText(0.2)
		);

		// Resizes the profile video on window resize
		$(window).on('resize orientationchange', function () {
			// TODO: Make this happen only if the profile card is open
			resizeProfileVideo();
		});

		// Close button
		$('.closeButton').click(function () {
			closeCard($(this).closest('.card'));
		});

		// If you click outside the card, it automatically closes the opened card
		$('.cardArea').click(function (event) {
			var target = event.target;
			if ($(target).hasClass('cardArea')) {
				closeCard($('.card.open'));
			}
		});
	})();

	//
	// Events
	//

	/**
	 * When clickcing on a card, open it up.
	 */
	$cards.click(function() {
		if (!cardIsOpen) {
			var $card = $(this);
			openCard($card);
		}
	});

	/**
	 * Called during steps of a card resizing
	 * @param {String} cardId The id of the card that is being resized
	 */
	function resizing (cardId) {
		fitTexts[cardId] = fitTexts[cardId] || [];
		fitTexts[cardId].forEach(function (resize) {
			resize();
		});

		// Special resizing cards
		switch (cardId) {
			case 'profile':
				resizeProfileVideo();
				break;
		}
	}

	/**
	 * Resizes the profile video and adjusts the position
	 */
	function resizeProfileVideo() {
		// Resize video
		var $video = $('.profileVideo');
		var videoWidth = $video.width();
		var videoHeight = $video.height();

		var $card = $video.closest('.card');
		var videoAreaHeight = $card.outerHeight();
		var videoAreaWidth = $card.outerWidth() / 3; // TODO: Make this more robust

		var videoRatio = videoHeight/videoWidth;
		var videoAreaRatio = videoAreaHeight/videoAreaWidth;

		var margin;
		// If videoArea too flat, make 100% height
		if (videoRatio < videoAreaHeight/videoAreaWidth) {
			margin = (videoAreaWidth - videoWidth)/2;
			$video.css({
				'width': 'auto',
				'height': '100%',
				'margin-top': 0,
				'margin-left': margin
			});
		} else {
			margin = (videoAreaHeight - videoHeight)/2;
			$video.css({
				'width': '100%',
				'height': 'auto',
				'margin-top': margin,
				'margin-left': 0
			});
		}
	}

	//
	// Actions
	//

	/**
	 * Opens a card. Expands it to fill the view
	 * @param {Card} $card The jquery-wrapped card
	 */
	function openCard ($card) {
		cardIsOpen = true;

		var cardId = $card.data('id');
		var cardIndex = $card.index();
		var siblingCardIndices = getRowIndices(cardIndex);
		var siblingCards = $(getCards(siblingCardIndices).filter(function (card) {
			return card !== $card[0];
		}));

		// Animate the clicked card to open
		$card.animate({
			width: OPEN_SIZE.WIDTH,
			height: OPEN_SIZE.HEIGHT
		}, {
			duration: ANIMATION_TIME,
			step: function () {
				resizing(cardId);
			},
			done: function () {

			}
		});

		// Animate the contents of the clicked card
		$card.find('.closed').show().fadeOut(ANIMATION_TIME);

		// Animate the sibling cards on the same row to closed
		siblingCards.each(function () {
			var $siblingCard = $(this);
			$(this).animate({
				width: HIDE_SIZE.WIDTH,
				height: OPEN_SIZE.HEIGHT
			}, ANIMATION_TIME, function () {
				$siblingCard.hide();
			});
		});

		// Set some properties on the opened card
		$card.addClass('open');
	}

	/**
	 * Closes a card.
	 * @param {Card} $card The jquery-wrapped card
	 */
	function closeCard ($card) {
		var cardId = $card.data('id');
		var cardIndex = $card.index();
		var siblingCardIndices = getRowIndices(cardIndex);
		var siblingCards = $(getCards(siblingCardIndices).filter(function (card) {
			return card !== $card[0];
		}));

		// Animate the clicked card to closed
		$card.animate({
			width: CLOSED_SIZE.WIDTH,
			height: CLOSED_SIZE.HEIGHT
		}, {
			duration: ANIMATION_TIME,
			step: function () {
				resizing(cardId);
			},
			done: function () {
				// Say card is open after all transitions are done
				cardIsOpen = false;
			}
		});

		// Animate the contents of the clicked card
		$card.find('.closed').hide().fadeIn(ANIMATION_TIME);

		// Animate the sibling cards on the same row to closed
		siblingCards.each(function () {
			var $siblingCard = $(this);
			$siblingCard.show();
			$(this).animate({
				width: CLOSED_SIZE.WIDTH,
				height: CLOSED_SIZE.HEIGHT
			}, ANIMATION_TIME);
		});

		$card.removeClass('open');
	}

	/**
	 * Gets an array of the indices of a certain row
	 * @param {Integer} cardIndex An index of a card to get the row from
	 * @returns {Integer[]} An array containing all the card indices of the specified row
	 */
	function getRowIndices(cardIndex) {
		var rowNum = Math.ceil((cardIndex + 1)/CARDS_PER_ROW) - 1;
		var colIndices = new Array(CARDS_PER_ROW);
		for (var i = 0; i < CARDS_PER_ROW; ++i) {
			colIndices[i] = rowNum * CARDS_PER_ROW + i;
		}
		return colIndices;
	}

	/**
	 * Gets the cards of a certain index or indices
	 * @param {Integer|Integer[]} indices The index/indices to get
	 * @returns {Card|Cards[]} The jquery-wrapped Card(s)
	 */
	function getCards (indices) {
		if (typeof indices === 'number') {
			return $cards[indices];
		} else { // Assume it's an array
			return indices.map(function(i) {
				return $cards[i];
			});
		}
	}
});<|endoftext|>


<commit_before>$(main);

function main() {
    $.getJSON('./data.json', function(d) {
        var data = attachColors(groupData(d));
        var state = {
            type: '',
            category: '',
            amount: ''
        };
        var update = updateAll.bind(undefined, $('.content.row'), data, state);

        $(window).on('resize', update);

        createControls($('.controls.row'), state, update);
        $('.controlsContainer .control:last-child').trigger('click');
        update();
    });

    function groupData(data) {
        var ret = {};

        Object.keys(data).forEach(function(type) {
            var name;

            data[type].forEach(function(v) {
                var d = v.data;

                name = getProviderName(v.name);

                if(!(name in ret)) ret[name] = {};
                if(!(v.type in ret[name])) ret[name][v.type] = {};

                if(type == 'uptime') {
                    d = d.map(function(v) {
                        return v * 100;
                    });
                }

                ret[name][v.type][type] = d;
            });
        });

        return ret;
    }

    function attachColors(data) {
        var i = 0;

        for(var name in data) {
            data[name]._color = getColor(i);

            i++;
        }

        return data;
    }

    function createControls($p, state, update) {
        createTypes($p, state, update);
        createAmounts($p, state, update);
        createCategories($p, state, update);
    }

    function createTypes($p, state, update) {
        $controls($p, state, update, 'types', 'type', ['ping', 'http', 'https']);
    }

    function createAmounts($p, state, update) {
        // TODO: replace with a slider?
        $controls($p, state, update, 'amounts', 'amount', [3, 7, 14]);
    }

    function createCategories($p, state, update) {
        $controls($p, state, update, 'categories', 'category', ['latency', 'uptime']);
    }

    function $controls($p, state, update, containerClass, itemClass, items) {
        var $e = $('<div>',
            {'class': 'small-12 large-4 columns controlsContainer ' + containerClass}).appendTo($p);
        items.forEach($control.bind(undefined, $e, state, itemClass, update));
    }

    function $control($p, state, type, update, name) {
        var $e = $('<a>', {'class': 'control ' + type, href: '#'}).text(name).on('click', function(e) {
            e.preventDefault();

            $e.siblings().removeClass('selected').removeClass('label');
            $e.addClass('selected label');

            state[type] = name;

            update();
        }).appendTo($p);
    }

    function updateAll($p, data, state) {
        updateChart($p, data, state);
        updateLegend($p, data, state);
    }

    function updateChart($p, data, state) {
        var $c = $('canvas.chart:first');
        var height = 400;
        var $e, width;

        if(!$c.length) {
            $e = $('<div>',
                {'class': 'canvasContainer small-12 large-9 columns'}).appendTo($p);

            $c = $('<canvas>', {'class': 'chart'}).appendTo($e);
        }

        width = $c.parent().width();

        $c.attr({width: width, height: height});

        var ctx = $c[0].getContext('2d');
        new Chart(ctx).Line(getData(data, state), {
            datasetFill: false,
            animation: false,
			pointDot : true,
			pointDotRadius : 3
        });
    }

    function updateLegend($p, data, state) {
        var $table = $('table.legend:first');
        var provider, color, $e, $header;

        if(!$table.length) {
            $e = $('<div>',
                {'class': 'legendContainer small-12 large-3 columns'}).appendTo($p);

            $table = $('<table>', {'class': 'legend'}).appendTo($e);
        }

        $table.empty();

        $header = $('<tr>').appendTo($table);

        $('<th>', {'class': 'colorLegend'}).appendTo($header);
        $('<th>', {'class': 'cdn'}).text('CDN').appendTo($header);
        $('<th>', {'class': 'category'}).text(title(state.category)).appendTo($header);

        for(var name in data) {
            provider = data[name];
            color = provider._color;

            if(state.type in provider) {
                var $row = $('<tr>').appendTo($table);
                var lowerName = name.toLowerCase();
                var values = provider[state.type][state.category];
                var value;

                if(values) value = average(values.slice(-state.amount)).toFixed(3);
                if(state.category == 'uptime') value += ' %';

                $('<td>', {'class': 'color ' + lowerName}).css('background-color',
                    color).appendTo($row);
                $('<td>', {'class': 'name ' + lowerName}).text(name).appendTo($row);
                $('<td>', {'class': 'value ' + lowerName}).text(value).appendTo($row);
            }
        }
    }

    function title(a) {
        if(!a) return;

        return a[0].toUpperCase() + a.slice(1);
    }

    function average(arr) {
        if(!arr) return;

        return sum(arr) / arr.length;
    }

    function sum(arr) {
        if(!arr) return;

        return arr.reduce(function(a, b) {
            return a + b;
        });
    }

    function getData(data, state) {
        return {
            labels: getLabels(state.amount),
            datasets: getDatasets(data, state)
        };
    }

    function getLabels(amount) {
        var ret = [];
        var i, d;

        for(i = 0; i < amount; i++) {
            d = new XDate();

            d.addDays(-(amount - i - 1));

            ret.push(d.toString('dd MMM'));
        }

        return ret;
    }

    function getDatasets(data, state) {
        var ret = [];
        var color;

        for(var cdn in data) {
            color = data[cdn]._color;

            if(!(state.type in data[cdn])) continue;
            if(!(state.category in data[cdn][state.type])) continue;

            ret.push({
                strokeColor: color,
                pointColor: color,
                pointStrokeColor: color,
                data: data[cdn][state.type][state.category].slice(-state.amount)
            });
        }

        return ret;
    }

    function getProviderName(fullname) {
        return fullname.split(' ')[0];
    }

    function getColor(index) {
        return [
            '#d84f44',
            '#aee3d6',
            '#f3d5a2',
            '#5d96d7',
            '#444',
            '#e388eb',
            '#aadd5e'
        ][index];
    }

    function unique(arr) {
        var ret = {};

        arr.forEach(function(v) {
            ret[v] = true;
        });

        return Object.keys(ret);
    }

    function prop(name) {
        return function(v) {
            return v[name];
        };
    }
}
<commit_msg>Render graph data in reverse order<commit_after>$(main);

function main() {
    $.getJSON('./data.json', function(d) {
        var data = attachColors(groupData(d));
        var state = {
            type: '',
            category: '',
            amount: ''
        };
        var update = updateAll.bind(undefined, $('.content.row'), data, state);

        $(window).on('resize', update);

        createControls($('.controls.row'), state, update);
        $('.controlsContainer .control:last-child').trigger('click');
        update();
    });

    function groupData(data) {
        var ret = {};

        Object.keys(data).forEach(function(type) {
            var name;

            data[type].forEach(function(v) {
                var d = v.data;

                name = getProviderName(v.name);

                if(!(name in ret)) ret[name] = {};
                if(!(v.type in ret[name])) ret[name][v.type] = {};

                if(type == 'uptime') {
                    d = d.map(function(v) {
                        return v * 100;
                    });
                }

                ret[name][v.type][type] = d.reverse(); /* from oldest to newest */
            });
        });

        return ret;
    }

    function attachColors(data) {
        var i = 0;

        for(var name in data) {
            data[name]._color = getColor(i);

            i++;
        }

        return data;
    }

    function createControls($p, state, update) {
        createTypes($p, state, update);
        createAmounts($p, state, update);
        createCategories($p, state, update);
    }

    function createTypes($p, state, update) {
        $controls($p, state, update, 'types', 'type', ['ping', 'http', 'https']);
    }

    function createAmounts($p, state, update) {
        // TODO: replace with a slider?
        $controls($p, state, update, 'amounts', 'amount', [3, 7, 14]);
    }

    function createCategories($p, state, update) {
        $controls($p, state, update, 'categories', 'category', ['latency', 'uptime']);
    }

    function $controls($p, state, update, containerClass, itemClass, items) {
        var $e = $('<div>',
            {'class': 'small-12 large-4 columns controlsContainer ' + containerClass}).appendTo($p);
        items.forEach($control.bind(undefined, $e, state, itemClass, update));
    }

    function $control($p, state, type, update, name) {
        var $e = $('<a>', {'class': 'control ' + type, href: '#'}).text(name).on('click', function(e) {
            e.preventDefault();

            $e.siblings().removeClass('selected').removeClass('label');
            $e.addClass('selected label');

            state[type] = name;

            update();
        }).appendTo($p);
    }

    function updateAll($p, data, state) {
        updateChart($p, data, state);
        updateLegend($p, data, state);
    }

    function updateChart($p, data, state) {
        var $c = $('canvas.chart:first');
        var height = 400;
        var $e, width;

        if(!$c.length) {
            $e = $('<div>',
                {'class': 'canvasContainer small-12 large-9 columns'}).appendTo($p);

            $c = $('<canvas>', {'class': 'chart'}).appendTo($e);
        }

        width = $c.parent().width();

        $c.attr({width: width, height: height});

        var ctx = $c[0].getContext('2d');
        new Chart(ctx).Line(getData(data, state), {
            datasetFill: false,
            animation: false,
			pointDot : true,
			pointDotRadius : 3
        });
    }

    function updateLegend($p, data, state) {
        var $table = $('table.legend:first');
        var provider, color, $e, $header;

        if(!$table.length) {
            $e = $('<div>',
                {'class': 'legendContainer small-12 large-3 columns'}).appendTo($p);

            $table = $('<table>', {'class': 'legend'}).appendTo($e);
        }

        $table.empty();

        $header = $('<tr>').appendTo($table);

        $('<th>', {'class': 'colorLegend'}).appendTo($header);
        $('<th>', {'class': 'cdn'}).text('CDN').appendTo($header);
        $('<th>', {'class': 'category'}).text(title(state.category)).appendTo($header);

        for(var name in data) {
            provider = data[name];
            color = provider._color;

            if(state.type in provider) {
                var $row = $('<tr>').appendTo($table);
                var lowerName = name.toLowerCase();
                var values = provider[state.type][state.category];
                var value;

                if(values) value = average(values.slice(-state.amount)).toFixed(3);
                if(state.category == 'uptime') value += ' %';

                $('<td>', {'class': 'color ' + lowerName}).css('background-color',
                    color).appendTo($row);
                $('<td>', {'class': 'name ' + lowerName}).text(name).appendTo($row);
                $('<td>', {'class': 'value ' + lowerName}).text(value).appendTo($row);
            }
        }
    }

    function title(a) {
        if(!a) return;

        return a[0].toUpperCase() + a.slice(1);
    }

    function average(arr) {
        if(!arr) return;

        return sum(arr) / arr.length;
    }

    function sum(arr) {
        if(!arr) return;

        return arr.reduce(function(a, b) {
            return a + b;
        });
    }

    function getData(data, state) {
        return {
            labels: getLabels(state.amount),
            datasets: getDatasets(data, state)
        };
    }

    function getLabels(amount) {
        var ret = [];
        var i, d;

        for(i = 0; i < amount; i++) {
            d = new XDate();

            d.addDays(-(amount - i - 1));

            ret.push(d.toString('dd MMM'));
        }

        return ret;
    }

    function getDatasets(data, state) {
        var ret = [];
        var color;

        for(var cdn in data) {
            color = data[cdn]._color;

            if(!(state.type in data[cdn])) continue;
            if(!(state.category in data[cdn][state.type])) continue;

            ret.push({
                strokeColor: color,
                pointColor: color,
                pointStrokeColor: color,
                data: data[cdn][state.type][state.category].slice(-state.amount)
            });
        }

        return ret;
    }

    function getProviderName(fullname) {
        return fullname.split(' ')[0];
    }

    function getColor(index) {
        return [
            '#d84f44',
            '#aee3d6',
            '#f3d5a2',
            '#5d96d7',
            '#444',
            '#e388eb',
            '#aadd5e'
        ][index];
    }

    function unique(arr) {
        var ret = {};

        arr.forEach(function(v) {
            ret[v] = true;
        });

        return Object.keys(ret);
    }

    function prop(name) {
        return function(v) {
            return v[name];
        };
    }
}
<|endoftext|>


<commit_before>var ledInfo = [];
var rootURL;

function init(){
    var colorpicker = $('#cp7').colorpicker({
    	format: "hex",
        color: '#ff0000',
        container: true,
        inline: true
    });

    d3.json('config.js', function(err, config){
    	rootURL = config.rootURL;

		//get LED info
	    $.ajax({url: rootURL + "/getLEDArrDimensions"}).then(
	    function(ledDimensions, err) { //TODO ERRORS
	    	//{width: 6, height: 4}
	       ledInfo = d3.range(0, ledInfo.width * ledInfo.height).map(function(d){return {};});;

	       var circleData = {radius : 5, spacing: 3};

	       var svg = d3.select("#ledArray").append("svg");
	       var ledGrp = svg.append("g");

	       var circleData = {radius: 5, spacing: 5};

	       ledGrp.selectAll("circle").data(ledInfo).enter().append("circle")
	       .attr({
		       	cx: function(d, i){return (Math.floor(i%ledDimensions.height) * 10) + 5;},
		       	cy: function(d, i){return (i * 10) + 5;},
		       	r: circleData.radius,
		       	"stroke-width": 1,
      			"stroke": "#000",
		       	"fill": "#fff",
		       	"id": function(d){return "led_" + d;}
	       }).on("click", function(e){
	       		//todo setLED and set led image on success
	       		$("#led_"+e).attr("fill", function(){return $('#cp7').colorpicker().data().color;});
	       });

	       refreshFromDevice();
	       //when done, fill in color
	    });
    });

    //todo set repeating call to get LED info and set picture
}

function refreshFromDevice(){
	$.ajax({ url: rootURL + "/getLEDPixels"}).then(
	function(particleLEDInfo, err) { //TODO ERRORS
    	for (var i = 0; i < ledInfo.length; i++) {
    		ledInfo[i].r = parseInt(particleLEDInfo[i].r, 16);
    		ledInfo[i].g = parseInt(particleLEDInfo[i].g, 16);
    		ledInfo[i].b = parseInt(particleLEDInfo[i].b, 16);
    	}
    });
}<commit_msg>YOLO commit some fun changes<commit_after>var ledInfo = [];
var rootURL;
var arrDims = {};

function init(){
    var colorpicker = $('#cp7').colorpicker({
    	format: "hex",
        color: '#ff0000',
        container: true,
        inline: true
    });

    d3.json('config.js', function(err, config){
    	rootURL = config.rootURL;

		//get LED info
	    $.ajax({url: rootURL + "/getLEDArrDimensions"}).then(
	    function(arrDimsOut, err) { //TODO ERRORS
	    	//{width: 6, height: 4}
	    	arrDims = arrDimsOut;
	       var ledInfo = d3.range(arrDims.width * arrDims.height).map(function(d){
		      var xy = getXY(d);
		      xy.addr = d;
		      return xy;
		    });

	       var circleData = {r : 10, spacing: 10};

	       var svg = d3.select("#ledArray").append("svg");
	       var ledGrp = svg.append("g").attr("transform", "scale(1,-1) translate(0,-" + (((circleData.r*2)+circleData.spacing)*arrDims.height) + ")");

	       ledGrp.selectAll("circle").data(ledInfo).enter().append("circle")
	       .attr({
      			cx: function(d){return d.x * (((ledProps.r*2)+ledProps.spacing))+ledProps.spacing;},
      			cy: function(d){return d.y * (((ledProps.r*2)+ledProps.spacing))+ledProps.spacing;},
		       	r: circleData.r,
		       	"stroke-width": 1,
      			"stroke": "#000",
		       	"fill": "#fff",
		       	"id": function(d){return "led_" + d;}
	       }).on("click", function(e){
	       		//todo setLED and set led image on success
	       		$("#led_"+e).attr("fill", function(){return $('#cp7').colorpicker().data().color;});
	       });

	       refreshFromDevice();
	       //when done, fill in color
	    });
    });

    //todo set repeating call to get LED info and set picture
}

function getXY(addr){
      var yNum = Math.floor(addr/arrDims.width);
      var xNum;
      if (yNum%2 ==0){ //even row
        xNum = addr - (yNum * arrDims.width);
      }
      else { //odd row
        xNum = ((yNum+1)*arrDims.width)-(addr+1);
      }

      return {x: xNum, y: yNum };
    }

function refreshFromDevice(){
	$.ajax({ url: rootURL + "/getLEDPixels"}).then(
	function(particleLEDInfo, err) { //TODO ERRORS
    	for (var i = 0; i < ledInfo.length; i++) {
    		ledInfo[i].r = parseInt(particleLEDInfo[i].r, 16);
    		ledInfo[i].g = parseInt(particleLEDInfo[i].g, 16);
    		ledInfo[i].b = parseInt(particleLEDInfo[i].b, 16);
    	}
    });
}
<|endoftext|>


<commit_before>jQuery(function($) {
    var validationRules = {
        login: [
            {
                regex: /^.+$/,
                alert: 'Login field is required'
            }, {
                regex: /^[a-z]/,
                alert: 'Login must begin with letter'
            }, {
                regex: /^[\w_]+$/,
                alert: 'Invalid characters entered'
            }, {
                regex: /^.{6,}$/,
                alert: 'Login must be at least 6 characters'
            }
        ],
        password: [
            {
                regex: /^.+$/,
                alert: 'Password field is required'
            }, {
                regex: /^.{8,}$/,
                alert: 'Password must be at least 8 characters'
            }, {
                regex: /^[\w]+$/,
                alert: 'Invalid characters entered'
            }
        ]
    };

    var $content = $('#content'),
        $form = $('#login-form'),
        $mask = $('#mask'),
        $modal;

    /**
     *
     */
    $.each(validationRules, function(name, rules) {
        var $input = $('[name="' +name + '"]', $form);

        if (!$input.size()) return;

        $input.on('change keyup blur click focus', function(e) {
            validate(name, rules, true);
        });
    });

    /**
     *
     * @param name
     * @param rules
     * @param check
     */
    function validate(name, rules, check) {
        var $input = $('[name="' +name + '"]', $form),
            $block = $input.closest('.control-group'),
            $hint  = $('.help-inline', $block),
            hasValidationErrors = false;

        $.each(rules, function(i, rule) {
            if (!hasValidationErrors) {
                if (rule.regex.test($input.val())) {
                    $hint.text('');
                    $block.addClass('success').removeClass('error warning');
                } else {
                    hasValidationErrors = true;
                    $hint.text(rule.alert);
                    $block.addClass(check ? 'warning' : 'error');
                    $block.removeClass(check ? 'error' : 'warning');
                    $block.removeClass('success');
                }
            }
        });

        return !hasValidationErrors;
    }

    /**
     * Modals
     */
    $mask.on('click', function() {
        $modal.fadeOut(200);
    });

    $('a[href^="#"]', $content).on('click', function(e) {
        var $target = $(this.hash);

        if ($target.length) {
            e.preventDefault();
            $target.css({
               top: (window.innerHeight - $target.height())/2,
               left: (window.innerWidth - $target.width())/2
            });
            $modal = $mask.add($target);
            $modal.fadeIn(300);
        }
    });

    /**
     *
     */
    $form.on('submit', function(e) {
        var isValid = true,
            ajaxUrl = $form.action; //our controller action which handle AJAX request

        e.preventDefault();

        $.each(validationRules, function(name, rules) {
            if (!validate(name, rules, false)) {
                isValid = false;
            }
        });

        if (!isValid) return;

        $.post(ajaxUrl, $form.serialize(), function(html) {
            if (html.length) {
                $modal.fadeOut(200);
                $content.html(html);
            }
        });
    });
});<commit_msg>Update main.js<commit_after>jQuery(function($) {
    var validationRules = {
        login: [
            {
                regex: /^.+$/,
                alert: 'Login field is required'
            }, {
                regex: /^[a-z]/,
                alert: 'Login must begin with letter'
            }, {
                regex: /^[\w_]+$/,
                alert: 'Invalid characters entered'
            }, {
                regex: /^.{6,}$/,
                alert: 'Login must be at least 6 characters'
            }
        ],
        password: [
            {
                regex: /^.+$/,
                alert: 'Password field is required'
            }, {
                regex: /^.{8,}$/,
                alert: 'Password must be at least 8 characters'
            }, {
                regex: /^[\w]+$/,
                alert: 'Invalid characters entered'
            }
        ]
    };

    var $content = $('#content'),
        $form = $('#login-form'),
        $mask = $('#mask'),
        $modal;

    /**
     *
     */
    $.each(validationRules, function(name, rules) {
        var $input = $('[name="' +name + '"]', $form);

        if (!$input.size()) return;

        $input.on('change keyup blur click focus', function(e) {
            validate(name, rules, true);
        });
    });

    /**
     *
     * @param name
     * @param rules
     * @param check
     */
    function validate(name, rules, check) {
        var $input = $('[name="' +name + '"]', $form),
            $block = $input.closest('.control-group'),
            $hint  = $('.help-inline', $block),
            hasValidationErrors = false;

        $.each(rules, function(i, rule) {
            if (!hasValidationErrors) {
                if (rule.regex.test($input.val())) {
                    $hint.text('');
                    $block.addClass('success').removeClass('error warning');
                } else {
                    hasValidationErrors = true;
                    $hint.text(rule.alert);
                    $block.addClass(check ? 'warning' : 'error');
                    $block.removeClass(check ? 'error' : 'warning');
                    $block.removeClass('success');
                }
            }
        });

        return !hasValidationErrors;
    }

    /**
     * Modals
     */
    $mask.on('click', function() {
        $modal.fadeOut(200);
    });

    $('a[href^="#"]', $content).on('click', function(e) {
        var $target = $(this.hash);

        if ($target.length) {
            e.preventDefault();
            $target.css({
               top: (window.innerHeight - $target.height())/2,
               left: (window.innerWidth - $target.width())/2
            });
            $modal = $mask.add($target);
            $modal.fadeIn(300);
        }
    });

    /**
     *
     */
    $form.on('submit', function(e) {
        var isValid = true,
            ajaxUrl = $form.action;

        e.preventDefault();

        $.each(validationRules, function(name, rules) {
            if (!validate(name, rules, false)) {
                isValid = false;
            }
        });

        if (!isValid) return;

        $.post(ajaxUrl, $form.serialize(), function(html) {
            if (html.length) {
                $modal.fadeOut(200);
                $content.html(html);
            }
        });
    });
});
<|endoftext|>