From b96d415fa5969f1a6510edc25c418b181d3295ca Mon Sep 17 00:00:00 2001 From: Bryan-Kirk Reinhardt Date: Mon, 4 May 2015 19:42:19 -0700 Subject: [PATCH 1/6] react - try again WIP --- client/react/css/app.css | 107 ++++++++++++++++++ client/react/index.html | 42 +++++++ client/react/js/app.js | 6 + .../react/js/components/LoginSection.react.js | 47 ++++++++ 4 files changed, 202 insertions(+) create mode 100644 client/react/css/app.css create mode 100644 client/react/index.html create mode 100644 client/react/js/app.js create mode 100644 client/react/js/components/LoginSection.react.js 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..66c3c73 --- /dev/null +++ b/client/react/index.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + Chattypantz Client Demo + + + + + + + + +
+
+

ChattyPantz Client Demo

+
+
+
+ + + + + + + + + + + diff --git a/client/react/js/app.js b/client/react/js/app.js new file mode 100644 index 0000000..dd1fb3f --- /dev/null +++ b/client/react/js/app.js @@ -0,0 +1,6 @@ +/** @jsx React.DOM */ +window.React = React; +React.render( + , + document.getElementById("chattypantzapp") +); diff --git a/client/react/js/components/LoginSection.react.js b/client/react/js/components/LoginSection.react.js new file mode 100644 index 0000000..d372e6f --- /dev/null +++ b/client/react/js/components/LoginSection.react.js @@ -0,0 +1,47 @@ +/** @jsx React.DOM */ + +var LoginSection = React.createClass({ + + getInitialState: function(){ + return { + nickname: "" + }; + }, + + componentDidMount: function() { + + }, + + componentWillUnmount: function(){ + + }, + + render: function() { + return( +
+
+
+
+ +
+
+ +
+
+
+
+ Error: +
+
Please try again later... +
+
+
+
+ ); + }, + + componentDidUpdate: function() { + + + } +}); From 2cea7fe0c4a7470ae9ae3b68c8557dc9675dd070 Mon Sep 17 00:00:00 2001 From: Bryan-Kirk Reinhardt Date: Wed, 6 May 2015 16:03:48 -0700 Subject: [PATCH 2/6] react continued --- client/react/README.md | 36 ++++++++++++ client/react/index.html | 1 + client/react/js/app.js | 1 + .../js/components/ConnectedSection.react.js | 56 +++++++++++++++++++ .../react/js/components/LoginSection.react.js | 29 ++++++++-- 5 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 client/react/README.md create mode 100644 client/react/js/components/ConnectedSection.react.js diff --git a/client/react/README.md b/client/react/README.md new file mode 100644 index 0000000..51a591a --- /dev/null +++ b/client/react/README.md @@ -0,0 +1,36 @@ +## ChattyPantz Client Demo - React.js + +NOTE: THIS IS A WORK IN PROGRESS AND NOT COMPLETED NOR FUNCTIONAL. FOR A DEMO PLEASE SEE ANGULAR DIRECTORY. + +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/index.html b/client/react/index.html index 66c3c73..39a95d9 100644 --- a/client/react/index.html +++ b/client/react/index.html @@ -36,6 +36,7 @@

ChattyPantz Client Demo

+ diff --git a/client/react/js/app.js b/client/react/js/app.js index dd1fb3f..a13f551 100644 --- a/client/react/js/app.js +++ b/client/react/js/app.js @@ -1,5 +1,6 @@ /** @jsx React.DOM */ window.React = React; +console.log("API") 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..000af7f --- /dev/null +++ b/client/react/js/components/ConnectedSection.react.js @@ -0,0 +1,56 @@ +/** @jsx React.DOM */ + +var ConnectedSection = React.createClass({ + + getInitialState: function(){ + return { + nickname: "" + }; + }, + + componentDidMount: function() { + + }, + + componentWillUnmount: function(){ + + }, + + render: function() { + return( +
+
+
+ +
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ ); + }, + + componentDidUpdate: function() { + + + } +}); diff --git a/client/react/js/components/LoginSection.react.js b/client/react/js/components/LoginSection.react.js index d372e6f..e8ded18 100644 --- a/client/react/js/components/LoginSection.react.js +++ b/client/react/js/components/LoginSection.react.js @@ -10,28 +10,34 @@ var LoginSection = React.createClass({ componentDidMount: function() { + }, componentWillUnmount: function(){ }, + componentWillMount: function(){ + }, + render: function() { return(
-
+
- +
- +
-
+
Error: -
+
chat.status.error
Please try again later...
@@ -43,5 +49,18 @@ var LoginSection = React.createClass({ componentDidUpdate: function() { + }, + + handleSubmit: function() { + React.unmountComponentAtNode(document.getElementById("chattypantzapp")) + }, + + // 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"; + } } }); From 7505e647d7fa7497e6cba33ea1c2b5151bece839 Mon Sep 17 00:00:00 2001 From: Bryan-Kirk Reinhardt Date: Fri, 8 May 2015 13:09:17 -0700 Subject: [PATCH 3/6] More exp --- client/react/index.html | 13 +- client/react/js/actions/ConnectionActions.js | 19 + client/react/js/actions/LoginActions.js | 9 + client/react/js/app.js | 9 +- .../js/components/ConnectedSection.react.js | 86 ++++- .../react/js/components/LoginSection.react.js | 84 ++-- client/react/js/constants/AppConstants.js | 9 + client/react/js/dispatcher/AppDispatcher.js | 4 + .../react/js/libs/facebook/flux/dist/Flux.js | 362 ++++++++++++++++++ .../react/js/libs/joyent/node/lib/events.js | 321 ++++++++++++++++ client/react/js/libs/joyent/node/lib/util.js | 15 + client/react/js/stores/ConnectionStore.js | 39 ++ client/react/js/stores/LoginStore.js | 37 ++ 13 files changed, 956 insertions(+), 51 deletions(-) create mode 100644 client/react/js/actions/ConnectionActions.js create mode 100644 client/react/js/actions/LoginActions.js create mode 100644 client/react/js/constants/AppConstants.js create mode 100644 client/react/js/dispatcher/AppDispatcher.js create mode 100755 client/react/js/libs/facebook/flux/dist/Flux.js create mode 100644 client/react/js/libs/joyent/node/lib/events.js create mode 100644 client/react/js/libs/joyent/node/lib/util.js create mode 100644 client/react/js/stores/ConnectionStore.js create mode 100644 client/react/js/stores/LoginStore.js diff --git a/client/react/index.html b/client/react/index.html index 39a95d9..1796b69 100644 --- a/client/react/index.html +++ b/client/react/index.html @@ -28,13 +28,24 @@

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..fc16542 --- /dev/null +++ b/client/react/js/actions/ConnectionActions.js @@ -0,0 +1,19 @@ +// 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 + }); + }, + + // 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..eb1e1e3 --- /dev/null +++ b/client/react/js/actions/LoginActions.js @@ -0,0 +1,9 @@ +// Helper functions for sending commands to the dispatcher from teh Login form. + +var LoginActions = { + login: function() { + AppDispatcher.dispatch({ + actionType: ActionTypes.LOGIN + }); + } +} diff --git a/client/react/js/app.js b/client/react/js/app.js index a13f551..813488a 100644 --- a/client/react/js/app.js +++ b/client/react/js/app.js @@ -1,7 +1,6 @@ -/** @jsx React.DOM */ -window.React = React; -console.log("API") +// Load initial state of the application. React.render( - , - document.getElementById("chattypantzapp") + , + document.getElementById("chattypantzapp") ); + diff --git a/client/react/js/components/ConnectedSection.react.js b/client/react/js/components/ConnectedSection.react.js index 000af7f..6f71ecd 100644 --- a/client/react/js/components/ConnectedSection.react.js +++ b/client/react/js/components/ConnectedSection.react.js @@ -1,20 +1,14 @@ -/** @jsx React.DOM */ - +// ConnectedSection is the interactive form for communication to the server once connected. var ConnectedSection = React.createClass({ + // setState(function|object nextState[, function callback]) + // forceUpdate([function callback]) - getInitialState: function(){ - return { - nickname: "" - }; - }, - - componentDidMount: function() { + // object propTypes + // array mixins + // object statics + // string displayName - }, - - componentWillUnmount: function(){ - - }, + // Built-in Component Methods render: function() { return( @@ -32,16 +26,17 @@ var ConnectedSection = React.createClass({
- +
- +
- +
- +
@@ -49,8 +44,61 @@ var ConnectedSection = React.createClass({ ); }, - componentDidUpdate: function() { + getInitialState: function() { + return null; + }, + getDefaultProps: function() { + // NOP + }, + + // Built-in Lifecycle Methods + + componentWillMount: function(){ + // NOP + }, + componentDidMount: function(){ + ConnectionStore.addChangeListener(this._onChange); + }, + componentWillReceiveProps: function(){ + // NOP + }, + shouldComponentUpdate: function(){ + // NOP + }, + componentWillUpdate: function(){ + // NOP + }, + componentDidUpdate: function(){ + // NOP + }, + + componentWillUnmount: function(){ + ConnectionStore.removeChangeListener(this._onChange); + }, + + // callback method for store to communicate any data change. + _onChange: function() { + // on call, you would pull data from the ConnectionStore for display + }, + // Send button pressed for message. + _handleSend: function() { + ConnectionActions.send("TODO The Message Goes Here."); + 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) { + React.findDOMNode(this.refs.sendButton).className = "ui primary submit button"; + } else { + React.findDOMNode(this.refs.sendButton).className = "ui primary submit button disabled"; + } + }, + // 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 index e8ded18..e32912d 100644 --- a/client/react/js/components/LoginSection.react.js +++ b/client/react/js/components/LoginSection.react.js @@ -1,35 +1,25 @@ -/** @jsx React.DOM */ - +// LoginSection prompts the user for a nickname and navigates the initial connection to the server. var LoginSection = React.createClass({ + // setState(function|object nextState[, function callback]) + // forceUpdate([function callback]) - getInitialState: function(){ - return { - nickname: "" - }; - }, - - componentDidMount: function() { - - - }, - - componentWillUnmount: function(){ + // object propTypes + // array mixins + // object statics + // string displayName - }, + // Built-in Component Methods - componentWillMount: function(){ - }, - - render: function() { + render: function(){ return(
-
+
- +
-
@@ -46,17 +36,59 @@ var LoginSection = React.createClass({ ); }, - componentDidUpdate: function() { + getInitialState: function() { + return { + nickname: "" + }; + }, + + getDefaultProps: function() { + // NOP + }, + + // Built-in Lifecycle Methods + + componentWillMount: function(){ + // NOP + }, + + componentDidMount: function(){ + LoginStore.addChangeListener(this._onChange); + }, + + componentWillReceiveProps: function(){ + // NOP + }, + shouldComponentUpdate: function(){ + // NOP + }, + componentWillUpdate: function(){ + // NOP + }, + componentDidUpdate: function(){ + // NOP + }, + + componentWillUnmount: function(){ + LoginStore.removeChangeListener(this._onChange); + }, + + // Custom Methods + // callback method for store to communicate any data change. + _onChange: function() { + // on call, you would pull data from the LoginStore for display }, - handleSubmit: function() { - React.unmountComponentAtNode(document.getElementById("chattypantzapp")) + // Submit button for login. + _handleSubmit: function() { + LoginActions.login(); + return false; }, // When the input field changes, check the length and disable the button if it is empty. - handleNicknameChange: function(event) { + _handleNicknameChange: function(event) { if(event.target.value.length > 0) { React.findDOMNode(this.refs.loginButton).className = "ui primary submit button"; } else { diff --git a/client/react/js/constants/AppConstants.js b/client/react/js/constants/AppConstants.js new file mode 100644 index 0000000..dc11552 --- /dev/null +++ b/client/react/js/constants/AppConstants.js @@ -0,0 +1,9 @@ +var EventTypes = { + CHANGE_EVENT: "change" +}; + +var ActionTypes = { + LOGIN: "LOGIN", + SEND_MESSAGE: "SEND_MESSAGE", + LOGOUT: "LOGOUT" +}; 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..a58b4de --- /dev/null +++ b/client/react/js/stores/ConnectionStore.js @@ -0,0 +1,39 @@ +// TBD +var ConnectionStore = Object.assign({}, EventEmitter.prototype, { + 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); + }, + + get: function(id) { + return null; + }, + + getAll: function(id) { + return null; + } + +}); + +ConnectionStore.dispatchToken = AppDispatcher.register(function(action) { + switch (action.actionType) { + case ActionTypes.SEND_MESSAGE: + console.log(action.message); + break; + case ActionTypes.LOGOUT: + React.render( + , + document.getElementById("chattypantzapp") + ); + 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..b45117c --- /dev/null +++ b/client/react/js/stores/LoginStore.js @@ -0,0 +1,37 @@ +// TBD +var _messages = {}; +var LoginStore = Object.assign({}, EventEmitter.prototype, { + 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); + }, + + get: function(id) { + return _messages[id]; + }, + + getAll: function(id) { + return _messages; + } + +}); + +LoginStore.dispatchToken = AppDispatcher.register(function(action) { + switch (action.actionType) { + case ActionTypes.LOGIN: + React.render( + , + document.getElementById("chattypantzapp") + ); + break; + default: + // do nothing. + } +}); From 5729fbd7e6edfa6177661467fc662968fc9ce112 Mon Sep 17 00:00:00 2001 From: Bryan-Kirk Reinhardt Date: Sat, 9 May 2015 20:21:02 -0700 Subject: [PATCH 4/6] Daily --- .../angular/app/scripts/controllers/main.js | 1 - client/react/js/actions/ConnectionActions.js | 8 +++- client/react/js/actions/LoginActions.js | 12 +++++- .../js/components/ConnectedSection.react.js | 6 +-- .../react/js/components/LoginSection.react.js | 26 ++++++++---- client/react/js/constants/AppConstants.js | 40 +++++++++++++++++++ client/react/js/stores/ConnectionStore.js | 39 ------------------ client/react/js/stores/LoginStore.js | 34 +++++++++++----- 8 files changed, 103 insertions(+), 63 deletions(-) delete mode 100644 client/react/js/stores/ConnectionStore.js diff --git a/client/angular/app/scripts/controllers/main.js b/client/angular/app/scripts/controllers/main.js index f1676fa..5f68afe 100755 --- a/client/angular/app/scripts/controllers/main.js +++ b/client/angular/app/scripts/controllers/main.js @@ -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/js/actions/ConnectionActions.js b/client/react/js/actions/ConnectionActions.js index fc16542..bf6199e 100644 --- a/client/react/js/actions/ConnectionActions.js +++ b/client/react/js/actions/ConnectionActions.js @@ -1,7 +1,6 @@ // 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({ @@ -10,6 +9,13 @@ var ConnectionActions = { }); }, + // Refresh the form elements + refresh: function() { + AppDispatcher.dispatch({ + actionType: ActionTypes.REFRESH_CONNECTION + }); + }, + // Logout of the server. logout: function() { AppDispatcher.dispatch({ diff --git a/client/react/js/actions/LoginActions.js b/client/react/js/actions/LoginActions.js index eb1e1e3..63c1763 100644 --- a/client/react/js/actions/LoginActions.js +++ b/client/react/js/actions/LoginActions.js @@ -1,9 +1,17 @@ // Helper functions for sending commands to the dispatcher from teh Login form. var LoginActions = { - login: function() { + login: function(nickname) { AppDispatcher.dispatch({ - actionType: ActionTypes.LOGIN + 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/components/ConnectedSection.react.js b/client/react/js/components/ConnectedSection.react.js index 6f71ecd..f3566fe 100644 --- a/client/react/js/components/ConnectedSection.react.js +++ b/client/react/js/components/ConnectedSection.react.js @@ -15,10 +15,10 @@ var ConnectedSection = React.createClass({
- +
- Online Users (chat.data.users.length) + Online Users {this.getUserCount()}
nickname
@@ -36,7 +36,7 @@ var ConnectedSection = React.createClass({ placeholder="Type message here..." />
- +
diff --git a/client/react/js/components/LoginSection.react.js b/client/react/js/components/LoginSection.react.js index e32912d..1522a85 100644 --- a/client/react/js/components/LoginSection.react.js +++ b/client/react/js/components/LoginSection.react.js @@ -1,6 +1,5 @@ // LoginSection prompts the user for a nickname and navigates the initial connection to the server. var LoginSection = React.createClass({ - // setState(function|object nextState[, function callback]) // forceUpdate([function callback]) // object propTypes @@ -12,22 +11,22 @@ var LoginSection = React.createClass({ render: function(){ return( -
+
-
+ ref="errorBox" id="connection-error"> Error: -
chat.status.error +
{this.state.error}
Please try again later...
@@ -39,7 +38,8 @@ var LoginSection = React.createClass({ getInitialState: function() { return { - nickname: "" + nickname: '', + error: '' }; }, @@ -78,12 +78,22 @@ var LoginSection = React.createClass({ // callback method for store to communicate any data change. _onChange: function() { - // on call, you would pull data from the LoginStore for display + this.setState({ + nickname: LoginStore.getNickname(), + error: LoginStore.getError() + }); + if(this.state.error != '') { + React.findDOMNode(this.refs.errorBox).className = "ui error message center aligned thirteen wide column"; + } else { + React.findDOMNode(this.refs.errorBox).className = "ui error message center aligned thirteen wide column hidden"; + } + this.forceUpdate(); }, // Submit button for login. _handleSubmit: function() { - LoginActions.login(); + React.findDOMNode(this.refs.nickname).value + LoginActions.login(React.findDOMNode(this.refs.nickname).value); return false; }, diff --git a/client/react/js/constants/AppConstants.js b/client/react/js/constants/AppConstants.js index dc11552..ab3fe41 100644 --- a/client/react/js/constants/AppConstants.js +++ b/client/react/js/constants/AppConstants.js @@ -4,6 +4,46 @@ var EventTypes = { var ActionTypes = { LOGIN: "LOGIN", + REFRESH_LOGIN: "REFRESH_LOGIN", 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_NICKNAME_MANDATORY: 1003, + ERR_ALREADY_JOINED: 1004, + ERR_NICKNAME_USED: 1005, + ERR_HIDDEN_NICKNAME: 1006, + ERR_NOT_IN_ROOM: 1007, + ERR_UNKNOWN_REQUEST: 1008 +}; diff --git a/client/react/js/stores/ConnectionStore.js b/client/react/js/stores/ConnectionStore.js deleted file mode 100644 index a58b4de..0000000 --- a/client/react/js/stores/ConnectionStore.js +++ /dev/null @@ -1,39 +0,0 @@ -// TBD -var ConnectionStore = Object.assign({}, EventEmitter.prototype, { - 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); - }, - - get: function(id) { - return null; - }, - - getAll: function(id) { - return null; - } - -}); - -ConnectionStore.dispatchToken = AppDispatcher.register(function(action) { - switch (action.actionType) { - case ActionTypes.SEND_MESSAGE: - console.log(action.message); - break; - case ActionTypes.LOGOUT: - React.render( - , - document.getElementById("chattypantzapp") - ); - break; - default: - // do nothing. - } -}); diff --git a/client/react/js/stores/LoginStore.js b/client/react/js/stores/LoginStore.js index b45117c..b90e21d 100644 --- a/client/react/js/stores/LoginStore.js +++ b/client/react/js/stores/LoginStore.js @@ -1,6 +1,9 @@ // TBD -var _messages = {}; var LoginStore = Object.assign({}, EventEmitter.prototype, { + + nickname: "", + error: "", + emitChange: function() { this.emit(EventTypes.CHANGE_EVENT); }, @@ -13,23 +16,36 @@ var LoginStore = Object.assign({}, EventEmitter.prototype, { this.removeListener(EventTypes.CHANGE_EVENT, callback); }, - get: function(id) { - return _messages[id]; + setNickname: function(nickname) { + this.nickname = nickname; + }, + + getNickname: function() { + return this.nickname; + }, + + setError: function(error) { + this.error = error; }, - getAll: function(id) { - return _messages; + getError: function() { + return this.error; } }); LoginStore.dispatchToken = AppDispatcher.register(function(action) { + var ls = LoginStore; + var cs = ConnectionStore; switch (action.actionType) { case ActionTypes.LOGIN: - React.render( - , - document.getElementById("chattypantzapp") - ); + ls.setNickname(action.nickname); + cs.login(); + break; + case ActionTypes.REFRESH_LOGIN: + ls.setNickname(action.nickname); + ls.setError(action.error); + ls.emitChange(); break; default: // do nothing. From be525b9b364b7a31c0b08f8f69a7b7dd91241497 Mon Sep 17 00:00:00 2001 From: Bryan-Kirk Reinhardt Date: Sat, 9 May 2015 20:21:21 -0700 Subject: [PATCH 5/6] daily --- client/react/js/stores/ConnectionStore.js | 150 ++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 client/react/js/stores/ConnectionStore.js diff --git a/client/react/js/stores/ConnectionStore.js b/client/react/js/stores/ConnectionStore.js new file mode 100644 index 0000000..3a867a0 --- /dev/null +++ b/client/react/js/stores/ConnectionStore.js @@ -0,0 +1,150 @@ +// Tracks information about the connection to the server and the display + +var ConnectionStore = Object.assign({}, EventEmitter.prototype, { + chat: { + socket: null, + status: { + error: null, + starting: false, + started: false + }, + data: { + users: [], + room: 'Demo', + nickname: '', + history: '', + messageText: '' + } + }, + + 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(); + }, + + getSocketError: function() { + return this.chat.status.error; + } + +}); + +ConnectionStore.chat.init = function() { + // 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; + this.status.starting = true; +}; + +//////// SOCKET EVENT HANDLERS //////// + +ConnectionStore.chat.onOpenWS = function(e) { + var csc = ConnectionStore.chat; + var ls = LoginStore; + csc.status.starting = false; + csc.status.started = true; + csc.status.error = null; + csc.data.nickname = ls.getNickname(); + // Set the nickname, join the demo room, and get a list of names. + 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: + csc.data.history += "Chattypantz server: " + response.content + '\n'; + csc.data.history += "Quitting Chattypantz.\n"; + break; + default: + csc.data.history += response.content + '\n'; + csc.data.history += "Quitting Chattypantz.\n"; + } +}; + +// Connection error. +ConnectionStore.chat.onErrorWS = function(e) { + var csc = ConnectionStore.chat; + var ls = LoginStore; + // Stop run and show err. + csc.status.error = "Server disconnected: " + e.reason; + csc.status.started = false; + ls.setError(csc.status.error); + if(typeof LoginSection != "undefined") { + LoginActions.refresh(csc.data.nickname, csc.status.error); + } else { + ls.setNickname(''); + 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) { + switch (action.actionType) { + case ActionTypes.SEND_MESSAGE: + break; + case ActionTypes.LOGOUT: + React.render( + , + document.getElementById("chattypantzapp") + ); + break; + default: + // do nothing. + } +}); From e0aa229e98a090a0db8963a34b5fe5d8a7b33f2d Mon Sep 17 00:00:00 2001 From: Bryan-Kirk Reinhardt Date: Sun, 10 May 2015 19:58:24 -0700 Subject: [PATCH 6/6] Cleanup and server bug on leave. --- client/README.md | 2 +- .../angular/app/scripts/controllers/main.js | 10 +-- client/react/README.md | 2 - .../js/components/ConnectedSection.react.js | 86 ++++++++++--------- .../react/js/components/LoginSection.react.js | 57 +++--------- client/react/js/constants/AppConstants.js | 11 +-- client/react/js/stores/ConnectionStore.js | 76 +++++++++------- client/react/js/stores/LoginStore.js | 12 ++- server/chat_room.go | 3 + 9 files changed, 122 insertions(+), 137 deletions(-) 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 5f68afe..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 } diff --git a/client/react/README.md b/client/react/README.md index 51a591a..f474e38 100644 --- a/client/react/README.md +++ b/client/react/README.md @@ -1,7 +1,5 @@ ## ChattyPantz Client Demo - React.js -NOTE: THIS IS A WORK IN PROGRESS AND NOT COMPLETED NOR FUNCTIONAL. FOR A DEMO PLEASE SEE ANGULAR DIRECTORY. - This folder contains a simple demonstration of a client connection to the server using javascript and html. ### Dependencies diff --git a/client/react/js/components/ConnectedSection.react.js b/client/react/js/components/ConnectedSection.react.js index f3566fe..dccefda 100644 --- a/client/react/js/components/ConnectedSection.react.js +++ b/client/react/js/components/ConnectedSection.react.js @@ -1,28 +1,16 @@ // ConnectedSection is the interactive form for communication to the server once connected. var ConnectedSection = React.createClass({ - // setState(function|object nextState[, function callback]) - // forceUpdate([function callback]) - - // object propTypes - // array mixins - // object statics - // string displayName - - // Built-in Component Methods - render: function() { return( -
+
@@ -33,7 +21,7 @@ var ConnectedSection = React.createClass({
+ ref="messageBox" placeholder="Type message here..." />
@@ -45,54 +33,68 @@ var ConnectedSection = React.createClass({ }, getInitialState: function() { - return null; - }, - getDefaultProps: function() { - // NOP + return { + users: [], + history: '' + }; }, - // Built-in Lifecycle Methods - - componentWillMount: function(){ - // NOP - }, componentDidMount: function(){ ConnectionStore.addChangeListener(this._onChange); }, - componentWillReceiveProps: function(){ - // NOP - }, - shouldComponentUpdate: function(){ - // NOP - }, - componentWillUpdate: function(){ - // NOP - }, - componentDidUpdate: function(){ - // NOP - }, 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() { - // on call, you would pull data from the ConnectionStore for display + 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() { - ConnectionActions.send("TODO The Message Goes Here."); + 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. + // 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) { - React.findDOMNode(this.refs.sendButton).className = "ui primary submit button"; + this._enableSendButton(); } else { - React.findDOMNode(this.refs.sendButton).className = "ui primary submit button disabled"; + this._disableSendButton(); } }, diff --git a/client/react/js/components/LoginSection.react.js b/client/react/js/components/LoginSection.react.js index 1522a85..ca3f31c 100644 --- a/client/react/js/components/LoginSection.react.js +++ b/client/react/js/components/LoginSection.react.js @@ -1,17 +1,8 @@ // LoginSection prompts the user for a nickname and navigates the initial connection to the server. var LoginSection = React.createClass({ - // forceUpdate([function callback]) - - // object propTypes - // array mixins - // object statics - // string displayName - - // Built-in Component Methods - render: function(){ return( -
+
@@ -23,8 +14,7 @@ var LoginSection = React.createClass({
-
+
Error:
{this.state.error}
Please try again later... @@ -35,64 +25,45 @@ var LoginSection = React.createClass({ ); }, - getInitialState: function() { return { - nickname: '', - error: '' + nickname: LoginStore.getNickname(), + error: LoginStore.getError() }; }, - getDefaultProps: function() { - // NOP - }, - - // Built-in Lifecycle Methods - componentWillMount: function(){ - // NOP + this.setState({ + nickname: LoginStore.getNickname(), + error: LoginStore.getError() + }); }, componentDidMount: function(){ LoginStore.addChangeListener(this._onChange); }, - componentWillReceiveProps: function(){ - // NOP - }, - shouldComponentUpdate: function(){ - // NOP - }, - componentWillUpdate: function(){ - // NOP - }, - componentDidUpdate: function(){ - // NOP - }, - componentWillUnmount: function(){ LoginStore.removeChangeListener(this._onChange); }, - // Custom Methods - // callback method for store to communicate any data change. _onChange: function() { this.setState({ nickname: LoginStore.getNickname(), error: LoginStore.getError() }); - if(this.state.error != '') { - React.findDOMNode(this.refs.errorBox).className = "ui error message center aligned thirteen wide column"; - } else { - React.findDOMNode(this.refs.errorBox).className = "ui error message center aligned thirteen wide column hidden"; + }, + + _errorClassCurrent: function() { + if(this.state.error == '') { + return "ui error message center aligned thirteen wide column hidden"; } - this.forceUpdate(); + return "ui error message center aligned thirteen wide column"; }, // Submit button for login. _handleSubmit: function() { - React.findDOMNode(this.refs.nickname).value LoginActions.login(React.findDOMNode(this.refs.nickname).value); return false; }, diff --git a/client/react/js/constants/AppConstants.js b/client/react/js/constants/AppConstants.js index ab3fe41..af2cfb6 100644 --- a/client/react/js/constants/AppConstants.js +++ b/client/react/js/constants/AppConstants.js @@ -5,6 +5,7 @@ var EventTypes = { var ActionTypes = { LOGIN: "LOGIN", REFRESH_LOGIN: "REFRESH_LOGIN", + REFRESH_CONNECTION: "REFRESH_CONNECTION", SEND_MESSAGE: "SEND_MESSAGE", LOGOUT: "LOGOUT" }; @@ -40,10 +41,10 @@ var ResponseTypes = { // 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 }; diff --git a/client/react/js/stores/ConnectionStore.js b/client/react/js/stores/ConnectionStore.js index 3a867a0..7f8ab89 100644 --- a/client/react/js/stores/ConnectionStore.js +++ b/client/react/js/stores/ConnectionStore.js @@ -4,16 +4,13 @@ var ConnectionStore = Object.assign({}, EventEmitter.prototype, { chat: { socket: null, status: { - error: null, - starting: false, - started: false + error: '', }, data: { users: [], room: 'Demo', nickname: '', history: '', - messageText: '' } }, @@ -33,32 +30,42 @@ var ConnectionStore = Object.assign({}, EventEmitter.prototype, { this.chat.init(); }, - getSocketError: function() { + 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; - this.status.starting = true; }; //////// SOCKET EVENT HANDLERS //////// ConnectionStore.chat.onOpenWS = function(e) { - var csc = ConnectionStore.chat; - var ls = LoginStore; - csc.status.starting = false; - csc.status.started = true; - csc.status.error = null; - csc.data.nickname = ls.getNickname(); - // Set the nickname, join the demo room, and get a list of names. + 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( @@ -95,33 +102,34 @@ ConnectionStore.chat.onMessageWS = function(message) { ConnectionActions.refresh(); break; case ResponseTypes.ERR_NICKNAME_USED: - csc.data.history += "Chattypantz server: " + response.content + '\n'; - csc.data.history += "Quitting Chattypantz.\n"; + ConnectionStore.setStatusError(response.content); + ConnectionActions.logout(); break; default: - csc.data.history += response.content + '\n'; - csc.data.history += "Quitting Chattypantz.\n"; + ConnectionStore.setStatusError(response.content); + ConnectionActions.logout(); } }; // Connection error. ConnectionStore.chat.onErrorWS = function(e) { - var csc = ConnectionStore.chat; - var ls = LoginStore; - // Stop run and show err. - csc.status.error = "Server disconnected: " + e.reason; - csc.status.started = false; - ls.setError(csc.status.error); - if(typeof LoginSection != "undefined") { - LoginActions.refresh(csc.data.nickname, csc.status.error); + 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 { - ls.setNickname(''); + LoginStore.setNickname(nickname); + LoginStore.setError(err); React.render( , document.getElementById("chattypantzapp") ); - } - + } }; // Sends a request to the server @@ -135,14 +143,18 @@ ConnectionStore.chat.sendRequest = function(room, type, 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: - React.render( - , - document.getElementById("chattypantzapp") - ); + csc.socket.close(); break; default: // do nothing. diff --git a/client/react/js/stores/LoginStore.js b/client/react/js/stores/LoginStore.js index b90e21d..55fce0c 100644 --- a/client/react/js/stores/LoginStore.js +++ b/client/react/js/stores/LoginStore.js @@ -35,17 +35,15 @@ var LoginStore = Object.assign({}, EventEmitter.prototype, { }); LoginStore.dispatchToken = AppDispatcher.register(function(action) { - var ls = LoginStore; - var cs = ConnectionStore; switch (action.actionType) { case ActionTypes.LOGIN: - ls.setNickname(action.nickname); - cs.login(); + LoginStore.setNickname(action.nickname); + ConnectionStore.login(); break; case ActionTypes.REFRESH_LOGIN: - ls.setNickname(action.nickname); - ls.setError(action.error); - ls.emitChange(); + 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()