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 + + + + + + + + +
+
+

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( +
+
+
+ +
+
+ Online Users {this.state.users.length} +
+ {this._displayUsers()} +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ ); + }, + + 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( +
+
+
+
+ +
+
+ +
+
+
+
+ Error: +
{this.state.error} +
Please try again later... +
+
+
+
+ ); + }, + + 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()