diff --git a/client/README.md b/client/README.md
index 223c5bb..ee713ad 100644
--- a/client/README.md
+++ b/client/README.md
@@ -1,3 +1,3 @@
## ChattyPantz Client Demos
-Subfolders in this directory demonstrate client side connections to the Chattypantz server.
+Subfolders in this directory demonstrate client side connections to the Chattypantz server. Two demos are presented using Angular.js and React.js toolkits.
diff --git a/client/angular/app/scripts/controllers/main.js b/client/angular/app/scripts/controllers/main.js
index f1676fa..5254f3d 100755
--- a/client/angular/app/scripts/controllers/main.js
+++ b/client/angular/app/scripts/controllers/main.js
@@ -32,11 +32,11 @@ angular.module('chattypantzApp').controller('MainCtrl', function($scope, $route,
// Error Conditions
ERR_ROOM_MANDATORY: 1001,
ERR_MAX_ROOMS_REACHED: 1002,
- ERR_NICKNAME_MANDATORY: 1003,
- ERR_ALREADY_JOINED: 1004,
- ERR_NICKNAME_USED: 1005,
- ERR_HIDDEN_NICKNAME: 1006,
- ERR_NOT_IN_ROOM: 1007,
+ ERR_ROOM_UNAVAILABLE: 1003,
+ ERR_NICKNAME_MANDATORY: 1004,
+ ERR_ALREADY_JOINED: 1005,
+ ERR_NICKNAME_USED: 1006,
+ ERR_HIDDEN_NICKNAME: 1007,
ERR_UNKNOWN_REQUEST: 1008
}
@@ -104,7 +104,6 @@ angular.module('chattypantzApp').controller('MainCtrl', function($scope, $route,
$scope.$apply();
break;
case RESPONSE_TYPE.LIST_NAMES:
- console.log("GOT HERE")
var users = angular.fromJson(response.list);
$scope.chat.data.users = users;
$scope.$apply();
diff --git a/client/react/README.md b/client/react/README.md
new file mode 100644
index 0000000..f474e38
--- /dev/null
+++ b/client/react/README.md
@@ -0,0 +1,34 @@
+## ChattyPantz Client Demo - React.js
+
+This folder contains a simple demonstration of a client connection to the server using javascript and html.
+
+### Dependencies
+
+* You must be connected to the internet to load CDN react.js, semnatic-ui.js, and jquery.js.
+* The server should be running on localhost:6660
+
+### Instructions
+
+Assumption: server is up and running on localhost:6660.
+
+If needed, you can change the API host and port endpoint in scripts/services/api.js:
+```
+var server = "ws://127.0.0.1:6660/v1.0/chat";
+```
+1. Load index.html in your browser.
+2. Enter a nickname.
+3. Connect to the server.
+
+You will be placed into room "Demo". A list of user nicknames from the room will also be displayed.
+NOTE: If your nickname already exists when logging into the room, you will be warned and disconnected.
+
+4. Now, send your messages.
+5. When you are done, disconnect from the server.
+
+### Enhancements
+
+The application doesn't demonstrate multi-room management, nor does it demonstrate the following request types:
+* GET_NICKNAME: 102
+* LIST_ROOMS: 103
+* HIDE: 106
+* UNHIDE: 107
diff --git a/client/react/css/app.css b/client/react/css/app.css
new file mode 100644
index 0000000..87f5626
--- /dev/null
+++ b/client/react/css/app.css
@@ -0,0 +1,107 @@
+/* Space out content a bit */
+body {
+ padding-top: 20px;
+ padding-bottom: 20px;
+}
+
+/* Everything but the jumbotron gets side spacing for mobile first views */
+.header,
+.marketing,
+.footer {
+ padding-left: 15px;
+ padding-right: 15px;
+}
+
+/* Custom page header */
+.header {
+ border-bottom: 1px solid #e5e5e5;
+}
+/* Make the masthead heading the same height as the navigation */
+.header h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ line-height: 40px;
+ padding-bottom: 19px;
+}
+
+/* Custom page footer */
+.footer {
+ padding-top: 19px;
+ margin-top: 30px;
+ color: #777;
+ border-top: 1px solid #e5e5e5;
+}
+
+/* Customize container */
+@media (min-width: 768px) {
+ .container {
+ max-width: 730px;
+ }
+}
+.container-narrow > hr {
+ margin: 30px 0;
+}
+
+/* Main marketing message and sign up button */
+.jumbotron {
+ text-align: center;
+ border-bottom: 1px solid #e5e5e5;
+}
+.jumbotron .btn {
+ font-size: 21px;
+ padding: 14px 24px;
+}
+
+/* Supporting marketing content */
+.marketing {
+ margin: 40px 0;
+}
+.marketing p + h4 {
+ margin-top: 28px;
+}
+
+/* Responsive: Portrait tablets and up */
+@media screen and (min-width: 768px) {
+ /* Remove the padding we set earlier */
+ .header,
+ .marketing,
+ .footer {
+ padding-left: 0;
+ padding-right: 0;
+ }
+ /* Space out the masthead */
+ .header {
+ margin-bottom: 30px;
+ }
+ /* Remove the bottom border on the jumbotron for visual effect */
+ .jumbotron {
+ border-bottom: 0;
+ }
+}
+
+
+/* CUSTOM CSS */
+
+#chat-history {
+ width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ text-align: left;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ height: 250px;
+}
+
+#connection-error {
+/* text-align: center;*/
+}
+
+#online-user-list {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ height: 150px;
+ text-align: left;
+}
+
+
diff --git a/client/react/index.html b/client/react/index.html
new file mode 100644
index 0000000..1796b69
--- /dev/null
+++ b/client/react/index.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+ Chattypantz Client Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/react/js/actions/ConnectionActions.js b/client/react/js/actions/ConnectionActions.js
new file mode 100644
index 0000000..bf6199e
--- /dev/null
+++ b/client/react/js/actions/ConnectionActions.js
@@ -0,0 +1,25 @@
+// Helper functions for sending commands to the dispatcher from the Connection Form.
+
+var ConnectionActions = {
+ // Send a message to the server.
+ send: function(message) {
+ AppDispatcher.dispatch({
+ actionType: ActionTypes.SEND_MESSAGE,
+ message: message
+ });
+ },
+
+ // Refresh the form elements
+ refresh: function() {
+ AppDispatcher.dispatch({
+ actionType: ActionTypes.REFRESH_CONNECTION
+ });
+ },
+
+ // Logout of the server.
+ logout: function() {
+ AppDispatcher.dispatch({
+ actionType: ActionTypes.LOGOUT
+ });
+ }
+}
diff --git a/client/react/js/actions/LoginActions.js b/client/react/js/actions/LoginActions.js
new file mode 100644
index 0000000..63c1763
--- /dev/null
+++ b/client/react/js/actions/LoginActions.js
@@ -0,0 +1,17 @@
+// Helper functions for sending commands to the dispatcher from teh Login form.
+
+var LoginActions = {
+ login: function(nickname) {
+ AppDispatcher.dispatch({
+ actionType: ActionTypes.LOGIN,
+ nickname: nickname
+ });
+ },
+ refresh: function(nickname, error) {
+ AppDispatcher.dispatch({
+ actionType: ActionTypes.REFRESH_LOGIN,
+ nickname: nickname,
+ error: error
+ });
+ }
+}
diff --git a/client/react/js/app.js b/client/react/js/app.js
new file mode 100644
index 0000000..813488a
--- /dev/null
+++ b/client/react/js/app.js
@@ -0,0 +1,6 @@
+// Load initial state of the application.
+React.render(
+ ,
+ document.getElementById("chattypantzapp")
+);
+
diff --git a/client/react/js/components/ConnectedSection.react.js b/client/react/js/components/ConnectedSection.react.js
new file mode 100644
index 0000000..dccefda
--- /dev/null
+++ b/client/react/js/components/ConnectedSection.react.js
@@ -0,0 +1,106 @@
+// ConnectedSection is the interactive form for communication to the server once connected.
+var ConnectedSection = React.createClass({
+ render: function() {
+ return(
+
+ );
+ },
+
+ getInitialState: function() {
+ return {
+ users: [],
+ history: ''
+ };
+ },
+
+ componentDidMount: function(){
+ ConnectionStore.addChangeListener(this._onChange);
+ },
+
+ componentWillUnmount: function(){
+ ConnectionStore.removeChangeListener(this._onChange);
+ },
+
+ _disableSendButton: function() {
+ React.findDOMNode(this.refs.sendButton).className = "ui primary submit button disabled";
+ },
+
+ _enableSendButton: function() {
+ React.findDOMNode(this.refs.sendButton).className = "ui primary submit button";
+ },
+
+ // callback method for store to communicate any data change.
+ _onChange: function() {
+ var csc = ConnectionStore.chat;
+ this.setState({
+ users: csc.data.users,
+ history: csc.data.history
+ });
+ var ch = React.findDOMNode(this.refs.chatHistory);
+ ch.value = this.state.history;
+ ch.scrollTop = ch.scrollHeight;
+ this.forceUpdate();
+ },
+
+ _displayUsers: function() {
+ var result = ""
+ return (
+
+ {this.state.users.map(function(user) {
+ return {user}
;
+ })}
+
+ );
+ },
+
+ // Send button pressed for message.
+ _handleSend: function() {
+ var msg = React.findDOMNode(this.refs.messageBox).value
+ ConnectionActions.send(msg);
+ React.findDOMNode(this.refs.messageBox).value = "";
+ this._disableSendButton();
+ return false;
+ },
+
+ // When the message field changes, check the length and disable the
+ // send button if it is empty.
+ _handleMessageChange: function(event) {
+ if(event.target.value.length > 0) {
+ this._enableSendButton();
+ } else {
+ this._disableSendButton();
+ }
+ },
+
+ // Quit button pressed, so disconnect; return to main page.
+ _handleQuit: function() {
+ ConnectionActions.logout();
+ return false;
+ }
+});
diff --git a/client/react/js/components/LoginSection.react.js b/client/react/js/components/LoginSection.react.js
new file mode 100644
index 0000000..ca3f31c
--- /dev/null
+++ b/client/react/js/components/LoginSection.react.js
@@ -0,0 +1,79 @@
+// LoginSection prompts the user for a nickname and navigates the initial connection to the server.
+var LoginSection = React.createClass({
+ render: function(){
+ return(
+
+ );
+ },
+
+ getInitialState: function() {
+ return {
+ nickname: LoginStore.getNickname(),
+ error: LoginStore.getError()
+ };
+ },
+
+ componentWillMount: function(){
+ this.setState({
+ nickname: LoginStore.getNickname(),
+ error: LoginStore.getError()
+ });
+ },
+
+ componentDidMount: function(){
+ LoginStore.addChangeListener(this._onChange);
+ },
+
+ componentWillUnmount: function(){
+ LoginStore.removeChangeListener(this._onChange);
+ },
+
+ // callback method for store to communicate any data change.
+ _onChange: function() {
+ this.setState({
+ nickname: LoginStore.getNickname(),
+ error: LoginStore.getError()
+ });
+ },
+
+ _errorClassCurrent: function() {
+ if(this.state.error == '') {
+ return "ui error message center aligned thirteen wide column hidden";
+ }
+ return "ui error message center aligned thirteen wide column";
+ },
+
+ // Submit button for login.
+ _handleSubmit: function() {
+ LoginActions.login(React.findDOMNode(this.refs.nickname).value);
+ return false;
+ },
+
+ // When the input field changes, check the length and disable the button if it is empty.
+ _handleNicknameChange: function(event) {
+ if(event.target.value.length > 0) {
+ React.findDOMNode(this.refs.loginButton).className = "ui primary submit button";
+ } else {
+ React.findDOMNode(this.refs.loginButton).className = "ui primary submit button disabled";
+ }
+ }
+});
diff --git a/client/react/js/constants/AppConstants.js b/client/react/js/constants/AppConstants.js
new file mode 100644
index 0000000..af2cfb6
--- /dev/null
+++ b/client/react/js/constants/AppConstants.js
@@ -0,0 +1,50 @@
+var EventTypes = {
+ CHANGE_EVENT: "change"
+};
+
+var ActionTypes = {
+ LOGIN: "LOGIN",
+ REFRESH_LOGIN: "REFRESH_LOGIN",
+ REFRESH_CONNECTION: "REFRESH_CONNECTION",
+ SEND_MESSAGE: "SEND_MESSAGE",
+ LOGOUT: "LOGOUT"
+};
+
+var Server = "ws://127.0.0.1:6660/v1.0/chat";
+
+// Requests sent to the server.
+var RequestTypes = {
+ SET_NICKNAME: 101,
+ GET_NICKNAME: 102,
+ LIST_ROOMS: 103,
+ JOIN: 104,
+ LIST_NAMES: 105,
+ HIDE: 106,
+ UNHIDE: 107,
+ MSG: 108,
+ LEAVE: 109
+};
+
+// Responses coming from the server.
+var ResponseTypes = {
+ // Command responses
+ SET_NICKNAME: 101,
+ GET_NICKNAME: 102,
+ LIST_ROOMS: 103,
+ JOIN: 104,
+ LIST_NAMES: 105,
+ HIDE: 106,
+ UNHIDE: 107,
+ MSG: 108,
+ LEAVE: 109,
+
+ // Error Conditions
+ ERR_ROOM_MANDATORY: 1001,
+ ERR_MAX_ROOMS_REACHED: 1002,
+ ERR_ROOM_UNAVAILABLE: 1003,
+ ERR_NICKNAME_MANDATORY: 1004,
+ ERR_ALREADY_JOINED: 1005,
+ ERR_NICKNAME_USED: 1006,
+ ERR_HIDDEN_NICKNAME: 1007,
+ ERR_UNKNOWN_REQUEST: 1008
+};
diff --git a/client/react/js/dispatcher/AppDispatcher.js b/client/react/js/dispatcher/AppDispatcher.js
new file mode 100644
index 0000000..33d7b1b
--- /dev/null
+++ b/client/react/js/dispatcher/AppDispatcher.js
@@ -0,0 +1,4 @@
+// Dispatcher is used as a route for the display of components in the application,
+// as well as respond to data requests from the server.
+
+var AppDispatcher = new Flux.Dispatcher();
diff --git a/client/react/js/libs/facebook/flux/dist/Flux.js b/client/react/js/libs/facebook/flux/dist/Flux.js
new file mode 100755
index 0000000..bca94e6
--- /dev/null
+++ b/client/react/js/libs/facebook/flux/dist/Flux.js
@@ -0,0 +1,362 @@
+! function(e) {
+ if ("object" == typeof exports && "undefined" != typeof module) module.exports = e();
+ else if ("function" == typeof define && define.amd) define([], e);
+ else {
+ var f;
+ "undefined" != typeof window ? f = window : "undefined" != typeof global ? f = global : "undefined" != typeof self && (f = self), f.Flux = e()
+ }
+}(function() {
+ var define, module, exports;
+ return (function e(t, n, r) {
+ function s(o, u) {
+ if (!n[o]) {
+ if (!t[o]) {
+ var a = typeof require == "function" && require;
+ if (!u && a) return a(o, !0);
+ if (i) return i(o, !0);
+ var f = new Error("Cannot find module '" + o + "'");
+ throw f.code = "MODULE_NOT_FOUND", f
+ }
+ var l = n[o] = {
+ exports: {}
+ };
+ t[o][0].call(l.exports, function(e) {
+ var n = t[o][1][e];
+ return s(n ? n : e)
+ }, l, l.exports, e, t, n, r)
+ }
+ return n[o].exports
+ }
+ var i = typeof require == "function" && require;
+ for (var o = 0; o < r.length; o++) s(r[o]);
+ return s
+ })({
+ 1: [function(require, module, exports) {
+ /**
+ * Copyright (c) 2014, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+ module.exports.Dispatcher = require('./lib/Dispatcher')
+
+ }, {
+ "./lib/Dispatcher": 2
+ }],
+ 2: [function(require, module, exports) {
+ /*
+ * Copyright (c) 2014, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Dispatcher
+ * @typechecks
+ */
+
+ "use strict";
+
+ var invariant = require('./invariant');
+
+ var _lastID = 1;
+ var _prefix = 'ID_';
+
+ /**
+ * Dispatcher is used to broadcast payloads to registered callbacks. This is
+ * different from generic pub-sub systems in two ways:
+ *
+ * 1) Callbacks are not subscribed to particular events. Every payload is
+ * dispatched to every registered callback.
+ * 2) Callbacks can be deferred in whole or part until other callbacks have
+ * been executed.
+ *
+ * For example, consider this hypothetical flight destination form, which
+ * selects a default city when a country is selected:
+ *
+ * var flightDispatcher = new Dispatcher();
+ *
+ * // Keeps track of which country is selected
+ * var CountryStore = {country: null};
+ *
+ * // Keeps track of which city is selected
+ * var CityStore = {city: null};
+ *
+ * // Keeps track of the base flight price of the selected city
+ * var FlightPriceStore = {price: null}
+ *
+ * When a user changes the selected city, we dispatch the payload:
+ *
+ * flightDispatcher.dispatch({
+ * actionType: 'city-update',
+ * selectedCity: 'paris'
+ * });
+ *
+ * This payload is digested by `CityStore`:
+ *
+ * flightDispatcher.register(function(payload) {
+ * if (payload.actionType === 'city-update') {
+ * CityStore.city = payload.selectedCity;
+ * }
+ * });
+ *
+ * When the user selects a country, we dispatch the payload:
+ *
+ * flightDispatcher.dispatch({
+ * actionType: 'country-update',
+ * selectedCountry: 'australia'
+ * });
+ *
+ * This payload is digested by both stores:
+ *
+ * CountryStore.dispatchToken = flightDispatcher.register(function(payload) {
+ * if (payload.actionType === 'country-update') {
+ * CountryStore.country = payload.selectedCountry;
+ * }
+ * });
+ *
+ * When the callback to update `CountryStore` is registered, we save a reference
+ * to the returned token. Using this token with `waitFor()`, we can guarantee
+ * that `CountryStore` is updated before the callback that updates `CityStore`
+ * needs to query its data.
+ *
+ * CityStore.dispatchToken = flightDispatcher.register(function(payload) {
+ * if (payload.actionType === 'country-update') {
+ * // `CountryStore.country` may not be updated.
+ * flightDispatcher.waitFor([CountryStore.dispatchToken]);
+ * // `CountryStore.country` is now guaranteed to be updated.
+ *
+ * // Select the default city for the new country
+ * CityStore.city = getDefaultCityForCountry(CountryStore.country);
+ * }
+ * });
+ *
+ * The usage of `waitFor()` can be chained, for example:
+ *
+ * FlightPriceStore.dispatchToken =
+ * flightDispatcher.register(function(payload) {
+ * switch (payload.actionType) {
+ * case 'country-update':
+ * flightDispatcher.waitFor([CityStore.dispatchToken]);
+ * FlightPriceStore.price =
+ * getFlightPriceStore(CountryStore.country, CityStore.city);
+ * break;
+ *
+ * case 'city-update':
+ * FlightPriceStore.price =
+ * FlightPriceStore(CountryStore.country, CityStore.city);
+ * break;
+ * }
+ * });
+ *
+ * The `country-update` payload will be guaranteed to invoke the stores'
+ * registered callbacks in order: `CountryStore`, `CityStore`, then
+ * `FlightPriceStore`.
+ */
+
+ function Dispatcher() {
+ this.$Dispatcher_callbacks = {};
+ this.$Dispatcher_isPending = {};
+ this.$Dispatcher_isHandled = {};
+ this.$Dispatcher_isDispatching = false;
+ this.$Dispatcher_pendingPayload = null;
+ }
+
+ /**
+ * Registers a callback to be invoked with every dispatched payload. Returns
+ * a token that can be used with `waitFor()`.
+ *
+ * @param {function} callback
+ * @return {string}
+ */
+ Dispatcher.prototype.register = function(callback) {
+ var id = _prefix + _lastID++;
+ this.$Dispatcher_callbacks[id] = callback;
+ return id;
+ };
+
+ /**
+ * Removes a callback based on its token.
+ *
+ * @param {string} id
+ */
+ Dispatcher.prototype.unregister = function(id) {
+ invariant(
+ this.$Dispatcher_callbacks[id],
+ 'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
+ id
+ );
+ delete this.$Dispatcher_callbacks[id];
+ };
+
+ /**
+ * Waits for the callbacks specified to be invoked before continuing execution
+ * of the current callback. This method should only be used by a callback in
+ * response to a dispatched payload.
+ *
+ * @param {array} ids
+ */
+ Dispatcher.prototype.waitFor = function(ids) {
+ invariant(
+ this.$Dispatcher_isDispatching,
+ 'Dispatcher.waitFor(...): Must be invoked while dispatching.'
+ );
+ for (var ii = 0; ii < ids.length; ii++) {
+ var id = ids[ii];
+ if (this.$Dispatcher_isPending[id]) {
+ invariant(
+ this.$Dispatcher_isHandled[id],
+ 'Dispatcher.waitFor(...): Circular dependency detected while ' +
+ 'waiting for `%s`.',
+ id
+ );
+ continue;
+ }
+ invariant(
+ this.$Dispatcher_callbacks[id],
+ 'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
+ id
+ );
+ this.$Dispatcher_invokeCallback(id);
+ }
+ };
+
+ /**
+ * Dispatches a payload to all registered callbacks.
+ *
+ * @param {object} payload
+ */
+ Dispatcher.prototype.dispatch = function(payload) {
+ invariant(!this.$Dispatcher_isDispatching,
+ 'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
+ );
+ this.$Dispatcher_startDispatching(payload);
+ try {
+ for (var id in this.$Dispatcher_callbacks) {
+ if (this.$Dispatcher_isPending[id]) {
+ continue;
+ }
+ this.$Dispatcher_invokeCallback(id);
+ }
+ } finally {
+ this.$Dispatcher_stopDispatching();
+ }
+ };
+
+ /**
+ * Is this Dispatcher currently dispatching.
+ *
+ * @return {boolean}
+ */
+ Dispatcher.prototype.isDispatching = function() {
+ return this.$Dispatcher_isDispatching;
+ };
+
+ /**
+ * Call the callback stored with the given id. Also do some internal
+ * bookkeeping.
+ *
+ * @param {string} id
+ * @internal
+ */
+ Dispatcher.prototype.$Dispatcher_invokeCallback = function(id) {
+ this.$Dispatcher_isPending[id] = true;
+ this.$Dispatcher_callbacks[id](this.$Dispatcher_pendingPayload);
+ this.$Dispatcher_isHandled[id] = true;
+ };
+
+ /**
+ * Set up bookkeeping needed when dispatching.
+ *
+ * @param {object} payload
+ * @internal
+ */
+ Dispatcher.prototype.$Dispatcher_startDispatching = function(payload) {
+ for (var id in this.$Dispatcher_callbacks) {
+ this.$Dispatcher_isPending[id] = false;
+ this.$Dispatcher_isHandled[id] = false;
+ }
+ this.$Dispatcher_pendingPayload = payload;
+ this.$Dispatcher_isDispatching = true;
+ };
+
+ /**
+ * Clear bookkeeping used for dispatching.
+ *
+ * @internal
+ */
+ Dispatcher.prototype.$Dispatcher_stopDispatching = function() {
+ this.$Dispatcher_pendingPayload = null;
+ this.$Dispatcher_isDispatching = false;
+ };
+
+
+ module.exports = Dispatcher;
+
+ }, {
+ "./invariant": 3
+ }],
+ 3: [function(require, module, exports) {
+ /**
+ * Copyright (c) 2014, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule invariant
+ */
+
+ "use strict";
+
+ /**
+ * Use invariant() to assert state which your program assumes to be true.
+ *
+ * Provide sprintf-style format (only %s is supported) and arguments
+ * to provide information about what broke and what you were
+ * expecting.
+ *
+ * The invariant message will be stripped in production, but the invariant
+ * will remain to ensure logic does not differ in production.
+ */
+
+ var invariant = function(condition, format, a, b, c, d, e, f) {
+ if (false) {
+ if (format === undefined) {
+ throw new Error('invariant requires an error message argument');
+ }
+ }
+
+ if (!condition) {
+ var error;
+ if (format === undefined) {
+ error = new Error(
+ 'Minified exception occurred; use the non-minified dev environment ' +
+ 'for the full error message and additional helpful warnings.'
+ );
+ } else {
+ var args = [a, b, c, d, e, f];
+ var argIndex = 0;
+ error = new Error(
+ 'Invariant Violation: ' +
+ format.replace(/%s/g, function() {
+ return args[argIndex++];
+ })
+ );
+ }
+
+ error.framesToPop = 1; // we don't care about invariant's own frame
+ throw error;
+ }
+ };
+
+ module.exports = invariant;
+
+ }, {}]
+ }, {}, [1])(1)
+});
diff --git a/client/react/js/libs/joyent/node/lib/events.js b/client/react/js/libs/joyent/node/lib/events.js
new file mode 100644
index 0000000..d547a8f
--- /dev/null
+++ b/client/react/js/libs/joyent/node/lib/events.js
@@ -0,0 +1,321 @@
+// Chattypantz Modified for demo
+
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+'use strict';
+
+var domain;
+// var util = require('util'); // ChapptyPantz: disabled. global
+
+function EventEmitter() {
+ EventEmitter.init.call(this);
+ }
+ // module.exports = EventEmitter; // ChapptyPantz: disabled. global
+
+// Backwards-compat with node 0.10.x
+EventEmitter.EventEmitter = EventEmitter;
+
+EventEmitter.usingDomains = false;
+
+EventEmitter.prototype.domain = undefined;
+EventEmitter.prototype._events = undefined;
+EventEmitter.prototype._maxListeners = undefined;
+
+// By default EventEmitters will print a warning if more than 10 listeners are
+// added to it. This is a useful default which helps finding memory leaks.
+EventEmitter.defaultMaxListeners = 10;
+
+EventEmitter.init = function() {
+ this.domain = null;
+ if (EventEmitter.usingDomains) {
+ // if there is an active domain, then attach to it.
+ domain = domain || require('domain');
+ if (domain.active && !(this instanceof domain.Domain)) {
+ this.domain = domain.active;
+ }
+ }
+
+ if (!this._events || this._events === Object.getPrototypeOf(this)._events)
+ this._events = {};
+
+ this._maxListeners = this._maxListeners || undefined;
+};
+
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
+ if (!util.isNumber(n) || n < 0 || isNaN(n))
+ throw TypeError('n must be a positive number');
+ this._maxListeners = n;
+ return this;
+};
+
+EventEmitter.prototype.emit = function emit(type) {
+ var er, handler, len, args, i, listeners;
+
+ if (!this._events)
+ this._events = {};
+
+ // If there is no 'error' event listener then throw.
+ if (type === 'error' && !this._events.error) {
+ er = arguments[1];
+ if (this.domain) {
+ if (!er)
+ er = new Error('Uncaught, unspecified "error" event.');
+ er.domainEmitter = this;
+ er.domain = this.domain;
+ er.domainThrown = false;
+ this.domain.emit('error', er);
+ } else if (er instanceof Error) {
+ throw er; // Unhandled 'error' event
+ } else {
+ throw Error('Uncaught, unspecified "error" event.');
+ }
+ return false;
+ }
+
+ handler = this._events[type];
+
+ if (util.isUndefined(handler))
+ return false;
+
+ if (this.domain && this !== process)
+ this.domain.enter();
+
+ if (util.isFunction(handler)) {
+ switch (arguments.length) {
+ // fast cases
+ case 1:
+ handler.call(this);
+ break;
+ case 2:
+ handler.call(this, arguments[1]);
+ break;
+ case 3:
+ handler.call(this, arguments[1], arguments[2]);
+ break;
+ // slower
+ default:
+ len = arguments.length;
+ args = new Array(len - 1);
+ for (i = 1; i < len; i++)
+ args[i - 1] = arguments[i];
+ handler.apply(this, args);
+ }
+ } else if (util.isObject(handler)) {
+ len = arguments.length;
+ args = new Array(len - 1);
+ for (i = 1; i < len; i++)
+ args[i - 1] = arguments[i];
+
+ listeners = handler.slice();
+ len = listeners.length;
+ for (i = 0; i < len; i++)
+ listeners[i].apply(this, args);
+ }
+
+ if (this.domain && this !== process)
+ this.domain.exit();
+
+ return true;
+};
+
+EventEmitter.prototype.addListener = function addListener(type, listener) {
+ var m;
+
+ if (!util.isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ if (!this._events)
+ this._events = {};
+
+ // To avoid recursion in the case that type === "newListener"! Before
+ // adding it to the listeners, first emit "newListener".
+ if (this._events.newListener)
+ this.emit('newListener', type,
+ util.isFunction(listener.listener) ?
+ listener.listener : listener);
+
+ if (!this._events[type])
+ // Optimize the case of one listener. Don't need the extra array object.
+ this._events[type] = listener;
+ else if (util.isObject(this._events[type]))
+ // If we've already got an array, just append.
+ this._events[type].push(listener);
+ else
+ // Adding the second element, need to change to array.
+ this._events[type] = [this._events[type], listener];
+
+ // Check for listener leak
+ if (util.isObject(this._events[type]) && !this._events[type].warned) {
+ var m;
+ if (!util.isUndefined(this._maxListeners)) {
+ m = this._maxListeners;
+ } else {
+ m = EventEmitter.defaultMaxListeners;
+ }
+
+ if (m && m > 0 && this._events[type].length > m) {
+ this._events[type].warned = true;
+ console.error('(node) warning: possible EventEmitter memory ' +
+ 'leak detected. %d %s listeners added. ' +
+ 'Use emitter.setMaxListeners() to increase limit.',
+ this._events[type].length, type);
+ console.trace();
+ }
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.once = function once(type, listener) {
+ if (!util.isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ var fired = false;
+
+ function g() {
+ this.removeListener(type, g);
+
+ if (!fired) {
+ fired = true;
+ listener.apply(this, arguments);
+ }
+ }
+
+ g.listener = listener;
+ this.on(type, g);
+
+ return this;
+};
+
+// emits a 'removeListener' event iff the listener was removed
+EventEmitter.prototype.removeListener =
+ function removeListener(type, listener) {
+ var list, position, length, i;
+
+ if (!util.isFunction(listener))
+ throw TypeError('listener must be a function');
+
+ if (!this._events || !this._events[type])
+ return this;
+
+ list = this._events[type];
+ length = list.length;
+ position = -1;
+
+ if (list === listener ||
+ (util.isFunction(list.listener) && list.listener === listener)) {
+ delete this._events[type];
+ if (this._events.removeListener)
+ this.emit('removeListener', type, listener);
+
+ } else if (util.isObject(list)) {
+ for (i = length; i-- > 0;) {
+ if (list[i] === listener ||
+ (list[i].listener && list[i].listener === listener)) {
+ position = i;
+ break;
+ }
+ }
+
+ if (position < 0)
+ return this;
+
+ if (list.length === 1) {
+ list.length = 0;
+ delete this._events[type];
+ } else {
+ list.splice(position, 1);
+ }
+
+ if (this._events.removeListener)
+ this.emit('removeListener', type, listener);
+ }
+
+ return this;
+ };
+
+EventEmitter.prototype.removeAllListeners =
+ function removeAllListeners(type) {
+ var key, listeners;
+
+ if (!this._events)
+ return this;
+
+ // not listening for removeListener, no need to emit
+ if (!this._events.removeListener) {
+ if (arguments.length === 0)
+ this._events = {};
+ else if (this._events[type])
+ delete this._events[type];
+ return this;
+ }
+
+ // emit removeListener for all listeners on all events
+ if (arguments.length === 0) {
+ for (key in this._events) {
+ if (key === 'removeListener') continue;
+ this.removeAllListeners(key);
+ }
+ this.removeAllListeners('removeListener');
+ this._events = {};
+ return this;
+ }
+
+ listeners = this._events[type];
+
+ if (util.isFunction(listeners)) {
+ this.removeListener(type, listeners);
+ } else if (Array.isArray(listeners)) {
+ // LIFO order
+ while (listeners.length)
+ this.removeListener(type, listeners[listeners.length - 1]);
+ }
+ delete this._events[type];
+
+ return this;
+ };
+
+EventEmitter.prototype.listeners = function listeners(type) {
+ var ret;
+ if (!this._events || !this._events[type])
+ ret = [];
+ else if (util.isFunction(this._events[type]))
+ ret = [this._events[type]];
+ else
+ ret = this._events[type].slice();
+ return ret;
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+ var ret;
+ if (!emitter._events || !emitter._events[type])
+ ret = 0;
+ else if (util.isFunction(emitter._events[type]))
+ ret = 1;
+ else
+ ret = emitter._events[type].length;
+ return ret;
+};
diff --git a/client/react/js/libs/joyent/node/lib/util.js b/client/react/js/libs/joyent/node/lib/util.js
new file mode 100644
index 0000000..f5ef4ce
--- /dev/null
+++ b/client/react/js/libs/joyent/node/lib/util.js
@@ -0,0 +1,15 @@
+// Chattypants modified and stripped down for demo purposes.
+var util = {
+ isFunction: function(arg) {
+ return typeof arg === 'function';
+ },
+ isNumber: function(arg) {
+ return typeof arg === 'number';
+ },
+ isObject: function isObject(arg) {
+ return typeof arg === 'object' && arg !== null;
+ },
+ isUndefined: function(arg) {
+ return arg === void 0;
+ },
+};
diff --git a/client/react/js/stores/ConnectionStore.js b/client/react/js/stores/ConnectionStore.js
new file mode 100644
index 0000000..7f8ab89
--- /dev/null
+++ b/client/react/js/stores/ConnectionStore.js
@@ -0,0 +1,162 @@
+// Tracks information about the connection to the server and the display
+
+var ConnectionStore = Object.assign({}, EventEmitter.prototype, {
+ chat: {
+ socket: null,
+ status: {
+ error: '',
+ },
+ data: {
+ users: [],
+ room: 'Demo',
+ nickname: '',
+ history: '',
+ }
+ },
+
+ emitChange: function() {
+ this.emit(EventTypes.CHANGE_EVENT);
+ },
+
+ addChangeListener: function(callback) {
+ this.on(EventTypes.CHANGE_EVENT, callback);
+ },
+
+ removeChangeListener: function(callback) {
+ this.removeListener(EventTypes.CHANGE_EVENT, callback);
+ },
+
+ login: function() {
+ this.chat.init();
+ },
+
+ setStatusError: function(error) {
+ this.chat.status.error = error;
+ },
+
+ getStatusError: function() {
+ return this.chat.status.error;
+ },
+
+ setNickname: function(nickname) {
+ this.chat.data.nickname = nickname;
+ },
+
+ getNickname: function() {
+ return this.chat.data.nickname;
+ }
+
+});
+
+ConnectionStore.chat.init = function() {
+ this.data.users = [];
+ this.data.history = '';
+ // Open a socket and register event handlers.
+ this.socket = new WebSocket(Server);
+ this.socket.onopen = this.onOpenWS;
+ this.socket.onmessage = this.onMessageWS;
+ this.socket.onerror = this.onErrorWS;
+ this.socket.onclose = this.onErrorWS;
+};
+
+//////// SOCKET EVENT HANDLERS ////////
+
+ConnectionStore.chat.onOpenWS = function(e) {
+ var cs = ConnectionStore;
+ var csc = cs.chat;
+ cs.setNickname(LoginStore.getNickname());
+ cs.setStatusError('');
+ csc.sendRequest('', RequestTypes.SET_NICKNAME, csc.data.nickname);
+ csc.sendRequest(csc.data.room, RequestTypes.JOIN, csc.data.room);
+ React.render(
+ ,
+ document.getElementById("chattypantzapp")
+ );
+};
+
+// Response arrived from the server.
+ConnectionStore.chat.onMessageWS = function(message) {
+ var csc = ConnectionStore.chat;
+ var response = JSON.parse(message.data);
+ switch (response.rspType) {
+ case ResponseTypes.SET_NICKNAME:
+ csc.data.history += "Chattypantz server: " + response.content + '\n';
+ ConnectionActions.refresh();
+ break;
+ case ResponseTypes.JOIN:
+ csc.data.users = response.list;
+ csc.data.history += "Chattypantz server: " + response.content + '\n';
+ ConnectionActions.refresh();
+ break;
+ case ResponseTypes.LIST_NAMES:
+ csc.data.users = response.list;
+ ConnectionActions.refresh();
+ break;
+ case ResponseTypes.MSG:
+ csc.data.history += response.content + '\n';
+ ConnectionActions.refresh();
+ break;
+ case ResponseTypes.LEAVE:
+ csc.data.users = response.list;
+ csc.data.history += "Chattypantz server: " + response.content + '\n';
+ ConnectionActions.refresh();
+ break;
+ case ResponseTypes.ERR_NICKNAME_USED:
+ ConnectionStore.setStatusError(response.content);
+ ConnectionActions.logout();
+ break;
+ default:
+ ConnectionStore.setStatusError(response.content);
+ ConnectionActions.logout();
+ }
+};
+
+// Connection error.
+ConnectionStore.chat.onErrorWS = function(e) {
+ var nickname = ConnectionStore.getNickname();
+ var err = ConnectionStore.getStatusError();
+
+ if(e.code != 1000) {
+ err = "Server disconnected: " + e.code + ' ' + e.reason;
+ }
+
+ if(document.getElementById("loginSection") != null) {
+ LoginActions.refresh(nickname, err);
+ } else {
+ LoginStore.setNickname(nickname);
+ LoginStore.setError(err);
+ React.render(
+ ,
+ document.getElementById("chattypantzapp")
+ );
+ }
+};
+
+// Sends a request to the server
+ConnectionStore.chat.sendRequest = function(room, type, content) {
+ this.socket.send(JSON.stringify({
+ roomName: room,
+ reqtype: type,
+ content: content
+ }));
+};
+
+// Register the store with the dispatcher.
+ConnectionStore.dispatchToken = AppDispatcher.register(function(action) {
+ var cs = ConnectionStore;
+ var csc = cs.chat;
+ switch (action.actionType) {
+ case ActionTypes.REFRESH_CONNECTION:
+ cs.emitChange();
+ break;
+ case ActionTypes.SEND_MESSAGE:
+ csc.sendRequest(csc.data.room, RequestTypes.MSG, action.message);
+ cs.emitChange();
+ break;
+ case ActionTypes.LOGOUT:
+ csc.socket.close();
+ break;
+ default:
+ // do nothing.
+ }
+});
diff --git a/client/react/js/stores/LoginStore.js b/client/react/js/stores/LoginStore.js
new file mode 100644
index 0000000..55fce0c
--- /dev/null
+++ b/client/react/js/stores/LoginStore.js
@@ -0,0 +1,51 @@
+// TBD
+var LoginStore = Object.assign({}, EventEmitter.prototype, {
+
+ nickname: "",
+ error: "",
+
+ emitChange: function() {
+ this.emit(EventTypes.CHANGE_EVENT);
+ },
+
+ addChangeListener: function(callback) {
+ this.on(EventTypes.CHANGE_EVENT, callback);
+ },
+
+ removeChangeListener: function(callback) {
+ this.removeListener(EventTypes.CHANGE_EVENT, callback);
+ },
+
+ setNickname: function(nickname) {
+ this.nickname = nickname;
+ },
+
+ getNickname: function() {
+ return this.nickname;
+ },
+
+ setError: function(error) {
+ this.error = error;
+ },
+
+ getError: function() {
+ return this.error;
+ }
+
+});
+
+LoginStore.dispatchToken = AppDispatcher.register(function(action) {
+ switch (action.actionType) {
+ case ActionTypes.LOGIN:
+ LoginStore.setNickname(action.nickname);
+ ConnectionStore.login();
+ break;
+ case ActionTypes.REFRESH_LOGIN:
+ LoginStore.setNickname(action.nickname);
+ LoginStore.setError(action.error);
+ LoginStore.emitChange();
+ break;
+ default:
+ // do nothing.
+ }
+});
diff --git a/server/chat_room.go b/server/chat_room.go
index 045c470..427ebc3 100644
--- a/server/chat_room.go
+++ b/server/chat_room.go
@@ -149,6 +149,9 @@ func (r *ChatRoom) message(q *ChatRequest) {
// leave removes the chatter from the room and notifies the group the chatter has left.
func (r *ChatRoom) leave(q *ChatRequest) {
+ if ok := r.isMember(q.Who); !ok {
+ return
+ }
name := q.Who.Nickname()
var names []string
r.mu.Lock()