├── venv ├── pip-selfcheck.json └── Include │ └── site │ └── python2.7 │ └── greenlet │ └── greenlet.h ├── requirements.txt ├── .gitignore ├── ChatServerWorkSample.pdf ├── static ├── js │ ├── dispatcher.js │ ├── actions │ │ ├── RoomMsgActions.js │ │ ├── UsersListActions.js │ │ └── RoomsActions.js │ ├── main.js │ ├── components │ │ ├── Layout.js │ │ ├── Header.js │ │ ├── Rooms.js │ │ ├── Name.js │ │ ├── UserList.js │ │ └── Room.js │ └── stores │ │ ├── RoomMsgStore.js │ │ ├── RoomsStore.js │ │ └── UsersStore.js ├── index.html └── css │ └── style.css ├── run.sh ├── templates └── index.html ├── webpack.config.js ├── package.json ├── run.py └── readme.md /venv/pip-selfcheck.json: -------------------------------------------------------------------------------- 1 | {"last_check":"2016-11-30T18:37:09Z","pypi_version":"9.0.1"} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.11.1 2 | Flask_SocketIO==2.7.2 3 | eventlet==0.19.0 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv 2 | .DS_Store 3 | *.pyc 4 | /node_modules 5 | /static/main.min.js 6 | -------------------------------------------------------------------------------- /ChatServerWorkSample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shisaq/chat-server/HEAD/ChatServerWorkSample.pdf -------------------------------------------------------------------------------- /static/js/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from 'flux'; 2 | 3 | export default new Dispatcher; 4 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Chat Server Startup 3 | 4 | virtualenv venv 5 | . ./venv/bin/activate 6 | pip install -r requirements.txt 7 | export FLASK_APP=run.py 8 | flask run 9 | -------------------------------------------------------------------------------- /static/js/actions/RoomMsgActions.js: -------------------------------------------------------------------------------- 1 | import dispatcher from '../dispatcher'; 2 | 3 | export function sendMessage(data) { 4 | dispatcher.dispatch({ 5 | type: 'SEND_MESSAGE', 6 | data 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /static/js/actions/UsersListActions.js: -------------------------------------------------------------------------------- 1 | import dispatcher from '../dispatcher'; 2 | 3 | export function pushName(name) { 4 | dispatcher.dispatch({ 5 | type: 'PUSH_USERNAME', 6 | name 7 | }); 8 | } 9 | 10 | export function popName() { 11 | dispatcher.dispatch({ 12 | type: 'POP_USERNAME' 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /static/js/actions/RoomsActions.js: -------------------------------------------------------------------------------- 1 | import dispatcher from '../dispatcher'; 2 | 3 | export function matchUser(data) { 4 | dispatcher.dispatch({ 5 | type: 'MATCH_USER', 6 | data 7 | }) 8 | } 9 | 10 | export function updateStatus(room) { 11 | dispatcher.dispatch({ 12 | type: 'UPDATE_STATUS', 13 | room 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chat Server 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | Chat Server 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 4 | import injectTapEventPlugin from 'react-tap-event-plugin'; 5 | injectTapEventPlugin(); 6 | import { SocketProvider } from 'socket.io-react'; 7 | import io from 'socket.io-client'; 8 | 9 | import Layout from './components/Layout'; 10 | 11 | const socket = io.connect(process.env.SOCKET_URL); 12 | 13 | const App = () => ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | const app = document.getElementById('app'); 22 | 23 | ReactDOM.render(, app); 24 | -------------------------------------------------------------------------------- /static/js/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {grey100} from 'material-ui/styles/colors'; 3 | 4 | import Header from './Header'; 5 | import Rooms from "./Rooms"; 6 | 7 | const styles = { 8 | containerStyle: { 9 | width: '100%', 10 | margin: '20px auto', 11 | backgroundColor: grey100 12 | }, 13 | titleStyle: { 14 | paddingLeft: 20 15 | } 16 | }; 17 | 18 | export default class Layout extends React.Component { 19 | constructor() { 20 | super(); 21 | } 22 | 23 | render() { 24 | return( 25 |
26 |

Chat Server

27 |
28 | 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var debug = process.env.NODE_ENV !== "production"; 2 | var webpack = require('webpack'); 3 | var path = require('path'); 4 | 5 | module.exports = { 6 | context: path.join(__dirname, "static"), 7 | devtool: debug ? "inline-sourcemap" : null, 8 | entry: "./js/main.js", 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.jsx?$/, 13 | exclude: /(node_modules|bower_components)/, 14 | loader: 'babel-loader', 15 | query: { 16 | presets: ['react', 'es2015', 'stage-0'], 17 | plugins: ['react-html-attrs', 'transform-decorators-legacy', 'transform-class-properties'] 18 | } 19 | } 20 | ] 21 | }, 22 | output: { 23 | path: __dirname + "/static/", 24 | filename: "main.min.js" 25 | }, 26 | plugins: debug ? [] : [ 27 | new webpack.optimize.DedupePlugin(), 28 | new webpack.optimize.OccurenceOrderPlugin(), 29 | new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }), 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /static/js/stores/RoomMsgStore.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | import dispatcher from '../dispatcher'; 4 | 5 | class RoomMsgStore extends EventEmitter { 6 | constructor() { 7 | super(); 8 | // read messages from localStorage, or build an object if there's no data 9 | this.msgs = JSON.parse(localStorage.getItem('msgs')) || {}; 10 | } 11 | 12 | getAll() { 13 | return this.msgs; 14 | } 15 | 16 | updateMsg(data) { 17 | this.msgs[data.room] = this.msgs[data.room] || []; 18 | this.msgs[data.room].push(data.msg); 19 | localStorage.setItem('msgs', JSON.stringify(this.msgs)); 20 | this.emit('pushNewMsg', data.room); 21 | } 22 | 23 | handleActions(action) { 24 | switch(action.type) { 25 | case 'SEND_MESSAGE': { 26 | this.updateMsg(action.data); 27 | } 28 | } 29 | } 30 | } 31 | 32 | const roomMsgStore = new RoomMsgStore; 33 | dispatcher.register(roomMsgStore.handleActions.bind(roomMsgStore)); 34 | export default roomMsgStore; 35 | -------------------------------------------------------------------------------- /static/js/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Paper from 'material-ui/Paper'; 3 | import { socketConnect } from 'socket.io-react'; 4 | import {cyan500} from 'material-ui/styles/colors'; 5 | 6 | import Name from './Name'; 7 | import UserList from './UserList'; 8 | 9 | const styles = { 10 | paperStyle: { 11 | height: 70, 12 | marginTop: 20 13 | }, 14 | headerStyle: { 15 | display: 'flex', 16 | flexWrap: 'wrap', 17 | padding: '0 20px', 18 | backgroundColor: cyan500 19 | } 20 | }; 21 | 22 | 23 | @socketConnect 24 | export default class Header extends React.Component { 25 | constructor() { 26 | super(); 27 | this.state = { 28 | name: '' 29 | }; 30 | } 31 | 32 | pushName(name) { 33 | this.setState({name}); 34 | } 35 | 36 | render() { 37 | return( 38 | 39 | 43 | 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-server-frontend", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "dependencies": { 7 | "babel-core": "^6.20.0", 8 | "babel-loader": "^6.2.9", 9 | "babel-plugin-add-module-exports": "^0.1.4", 10 | "babel-plugin-react-html-attrs": "^2.0.0", 11 | "babel-plugin-transform-class-properties": "^6.19.0", 12 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 13 | "babel-preset-es2015": "^6.18.0", 14 | "babel-preset-react": "^6.16.0", 15 | "babel-preset-stage-0": "^6.16.0", 16 | "flux": "^3.1.0", 17 | "material-ui": "^0.16.5", 18 | "react": "^0.14.8", 19 | "react-dom": "^0.14.8", 20 | "react-tap-event-plugin": "^0.2.2", 21 | "react-timeout": "^1.0.0", 22 | "socket.io-client": "^1.7.2", 23 | "socket.io-react": "^1.1.2", 24 | "webpack": "^1.14.0", 25 | "webpack-dev-server": "^1.16.2" 26 | }, 27 | "devDependencies": {}, 28 | "scripts": { 29 | "dev": "webpack-dev-server --content-base static --inline --hot", 30 | "test": "echo \"Error: no test specified\" && exit 1", 31 | "build": "NODE_ENV=production webpack" 32 | }, 33 | "author": "Haiquan Li", 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | *, 2 | :before, 3 | :after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body, 9 | div, 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | h5, 15 | h6, 16 | p, 17 | ul, 18 | li { 19 | margin: 0; 20 | padding: 0; 21 | font-family: 'Roboto', sans-serif; 22 | } 23 | 24 | body { 25 | background-color: #F5F5F5; 26 | } 27 | 28 | h1,h2,h3,h4,h5,h6 { 29 | line-height: 1.2; 30 | } 31 | 32 | h1 { 33 | font-size: 48px; 34 | font-weight: 200; 35 | color: #1c0656; 36 | } 37 | 38 | h2 { 39 | font-size: 40px; 40 | font-weight: 200; 41 | color: #1c0656; 42 | margin: .5em 0; 43 | } 44 | 45 | h3 { 46 | font-size: 32px; 47 | letter-spacing: -0.5px; 48 | font-weight: 300; 49 | color: #1c0656; 50 | margin: 1em 0; 51 | } 52 | 53 | h4 { 54 | font-size: 18px; 55 | font-weight: 600; 56 | letter-spacing: -0.125px; 57 | color: #1c0656; 58 | } 59 | 60 | h4:not(:first-child) { 61 | margin: 3em 0 1.2em; 62 | } 63 | 64 | h5 { 65 | font-weight: 600; 66 | font-size: 16px; 67 | margin-bottom: .4em; 68 | } 69 | 70 | h6 { 71 | font-size: 14px; 72 | font-weight: 600; 73 | margin-bottom: .4em; 74 | } 75 | 76 | .hide { 77 | display: none !important; 78 | } 79 | -------------------------------------------------------------------------------- /static/js/stores/RoomsStore.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | import dispatcher from '../dispatcher'; 4 | 5 | class RoomsStore extends EventEmitter { 6 | constructor() { 7 | super(); 8 | this.rooms = []; 9 | } 10 | 11 | getAll() { 12 | return this.rooms; 13 | } 14 | 15 | matchUser(data) { 16 | for (var i = 0; i < this.rooms.length; i++) { 17 | if (data.room === this.rooms[i].room) { 18 | // console.log('This room has already been there.'); 19 | this.rooms[i].isActive = true; 20 | this.emit('addNewRoom', data); 21 | return; 22 | } 23 | } 24 | if (sessionStorage.name === data.inviter || 25 | sessionStorage.name === data.guest) { 26 | // console.log('Ready to make a new room out!!!'); 27 | this.rooms.push(data); 28 | this.emit('addNewRoom', data); 29 | } 30 | } 31 | 32 | updateStatus(room) { 33 | for (var i = 0; i < this.rooms.length; i++) { 34 | if (room === this.rooms[i].room) { 35 | // console.log(this.rooms[i].isActive); 36 | this.rooms[i].isActive = false; 37 | this.emit('updateRooms'); 38 | } 39 | } 40 | } 41 | 42 | handleActions(action) { 43 | switch(action.type) { 44 | case 'MATCH_USER': { 45 | this.matchUser(action.data); 46 | } 47 | case 'UPDATE_STATUS': { 48 | this.updateStatus(action.room); 49 | } 50 | } 51 | } 52 | } 53 | 54 | const roomsStore = new RoomsStore; 55 | dispatcher.register(roomsStore.handleActions.bind(roomsStore)); 56 | export default roomsStore; 57 | -------------------------------------------------------------------------------- /static/js/components/Rooms.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { socketConnect } from 'socket.io-react'; 3 | 4 | import Room from './Room'; 5 | import RoomsStore from '../stores/RoomsStore'; 6 | import * as RoomsActions from '../actions/RoomsActions'; 7 | 8 | const roomsStyle = { 9 | width: '100%', 10 | margin: '20px auto', 11 | padding: '20px', 12 | listStyle: 'none' 13 | }; 14 | 15 | @socketConnect 16 | export default class Rooms extends React.Component { 17 | constructor() { 18 | super(); 19 | this.state = { 20 | rooms: RoomsStore.getAll() 21 | }; 22 | } 23 | 24 | componentWillMount() { 25 | RoomsStore.on('addNewRoom', (data) => { 26 | this.setState({ 27 | rooms: RoomsStore.getAll() 28 | }); 29 | // 1 room and 2 users have been comfirmed. Now join them into the room. 30 | this.props.socket.emit('join_private_room', data); 31 | }); 32 | 33 | RoomsStore.on('updateRooms', () => { 34 | this.setState({ 35 | rooms: RoomsStore.getAll() 36 | }); 37 | }); 38 | 39 | this.props.socket.on('invite_match_user', (data) => { 40 | // Let's go and see if the username belongs to the room! 41 | RoomsActions.matchUser(data); 42 | }); 43 | } 44 | 45 | render() { 46 | const {rooms} = this.state; 47 | const roomComponents = rooms.map((data) => { 48 | return ; 49 | }); 50 | 51 | return( 52 |
53 |
    54 | {roomComponents} 55 |
56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /static/js/stores/UsersStore.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | import dispatcher from '../dispatcher'; 4 | 5 | class UsersStore extends EventEmitter { 6 | constructor() { 7 | super(); 8 | this.usersList = [ 9 | { 10 | name: 'Select a user...', 11 | timeStamp: null 12 | } 13 | ]; 14 | } 15 | 16 | getUsername(name) { 17 | for(var i = 1; i < this.usersList.length; i++) { 18 | if (name === this.usersList[i].name) { 19 | this.usersList[i].timeStamp = Date.now(); 20 | this.emit('updateUsersList'); 21 | return; 22 | } 23 | } 24 | // console.log('name does not equal, it will be added.'); 25 | this.usersList.push({ 26 | name, 27 | timeStamp: Date.now() 28 | }); 29 | 30 | this.emit('updateUsersList'); 31 | } 32 | 33 | popUsername() { 34 | setInterval( 35 | () => { 36 | for(var i = 1; i < this.usersList.length; i++) { 37 | if (Date.now() - this.usersList[i].timeStamp > 3500) { 38 | const invalidName = this.usersList[i].name; 39 | // console.log('User: [' + invalidName + '] is offline.'); 40 | this.usersList.splice(i, 1); 41 | this.emit('updateUsersList', invalidName); 42 | } 43 | } 44 | }, 45 | 3000); 46 | } 47 | 48 | getAll() { 49 | return this.usersList; 50 | } 51 | 52 | handleActions(action) { 53 | switch(action.type) { 54 | case 'PUSH_USERNAME': { 55 | this.getUsername(action.name); 56 | } 57 | case 'POP_USERNAME': { 58 | this.popUsername(); 59 | } 60 | } 61 | } 62 | } 63 | 64 | const usersStore = new UsersStore; 65 | dispatcher.register(usersStore.handleActions.bind(usersStore)); 66 | export default usersStore; 67 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import eventlet 3 | eventlet.monkey_patch() 4 | from flask import Flask, render_template 5 | from flask_socketio import SocketIO, emit, join_room, leave_room, rooms 6 | from datetime import datetime 7 | # to generate a random secret key 8 | import os 9 | # refresh pyc everytime I change code 10 | import sys 11 | sys.dont_write_bytecode = True 12 | 13 | # init 14 | async_mode = 'eventlet' 15 | 16 | app = Flask(__name__) 17 | app.secret_key = os.urandom(24) 18 | socketio = SocketIO(app, async_mode=async_mode) 19 | 20 | 21 | # index 22 | @app.route('/') 23 | def index(): 24 | return render_template('index.html') 25 | 26 | 27 | # socketio events 28 | # on connect 29 | @socketio.on('connect') 30 | def user_connect(): 31 | print(datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ' - Server connected successfully.') 32 | 33 | # offline 34 | @socketio.on('disconnect') 35 | def user_disconnect(): 36 | print(datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ' - Client disconnected.') 37 | 38 | # a user press enter after input a valid username 39 | @socketio.on('is_online') 40 | def user_joined(name): 41 | emit('online_name', name, broadcast=True) 42 | 43 | # `inviter` selects `guest`, sending 2 usernames 44 | @socketio.on('build_private_room') 45 | def creat_private_room(names): 46 | print('The inviter is: [' + names['inviter'] + '], the guest is: [' + names['guest'] + ']') 47 | r = [names['inviter'], names['guest']] 48 | r.sort() 49 | room = ''.join(r) 50 | emit('invite_match_user', { 51 | 'inviter': names['inviter'], 52 | 'guest': names['guest'], 53 | 'room': room, 54 | 'isActive': 'true' 55 | }, broadcast=True) 56 | 57 | # 2 filtered users emit this event, then join room 58 | @socketio.on('join_private_room') 59 | def the_private_room(data): 60 | join_room(data['room']) 61 | print('Users: ' + data['inviter'] + ', ' + data['guest'] + ' joined the room [' + data['room'] + '].') 62 | 63 | # send private messages 64 | @socketio.on('private_message') 65 | def handle_message(data): 66 | emit('room_message', data, room=data['room']) 67 | 68 | # startup 69 | if __name__ == '__main__': 70 | socketio.run(app, debug=True) 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Chat server 2 | 3 | > This is a simple sample of an online chat server. 4 | 5 | ## Requirement 6 | 1. Have Python 2.7 installed globally; 7 | 2. Have [pip](https://pip.pypa.io/en/stable/installing/) and [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) installed globally. 8 | 9 | ## Installation 10 | 1. Download this repository 11 | `$ git clone https://github.com/shisaq/chat-server.git` 12 | 2. On the root of this repository, open your terminal 13 | 3. Get the permission to read, write and execute: `$ chmod 755 ./run.sh` 14 | 4. Run `$ ./run.sh` to install dependencies 15 | ![server running](http://7xpx1z.com1.z0.glb.clouddn.com/github/serverrunning.png) 16 | 5. Open your browser, type `localhost:5000` or `127.0.0.1:5000` 17 | ![index](http://7xpx1z.com1.z0.glb.clouddn.com/github/chatserverindex.png) 18 | 6. (optional) If you prefer, you can run `npm install` to customize the front-end works 19 | 20 | ## How it looks 21 | 22 | ### Hit enter to register 23 | ![](http://7xpx1z.com1.z0.glb.clouddn.com/chatserver/chatserverinit.gif) 24 | 25 | ### Users list 26 | ![](http://7xpx1z.com1.z0.glb.clouddn.com/chatserver/chatserverdynamicrefreshuserlist.gif) 27 | 28 | ### Popup private room 29 | ![](http://7xpx1z.com1.z0.glb.clouddn.com/chatserver/chatserverselectuser.gif) 30 | 31 | ### Caller and callee popup together 32 | ![](http://7xpx1z.com1.z0.glb.clouddn.com/chatserver/chatserverpopuptogether.gif) 33 | 34 | ### Hit enter to send message 35 | ![](http://7xpx1z.com1.z0.glb.clouddn.com/chatserver/chatserverreceiveintime.gif) 36 | 37 | ### Private messages 38 | ![](http://7xpx1z.com1.z0.glb.clouddn.com/chatserver/chatserverprivate.gif) 39 | 40 | ### Users list refresh dynamically 41 | ![](http://7xpx1z.com1.z0.glb.clouddn.com/chatserver/chatserverofflinerefresh.gif) 42 | 43 | ## Technique stack 44 | 45 | * [Flask](http://flask.pocoo.org/) 46 | * [SocketIO](http://socket.io/) 47 | * [React](https://facebook.github.io/react/) 48 | 49 | ## Known issues 50 | 51 | * Scorll bar cannot scroll to bottom automatically when finished initially loading the room messages. 52 | * Messages actually store in localStorage. They may conflate when another user login at the same computer with the prior one. 53 | * It has not fitted for multiple sizes of screens. 54 | * Sometimes it may judge a user offline, due to unstable network. 55 | * Rooms list is an array now. But it should be an object, including every item named by the room name. Just like Room messages object. 56 | 57 | ## Liscense 58 | 59 | MIT 60 | -------------------------------------------------------------------------------- /static/js/components/Name.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import ReactTimeout from 'react-timeout'; 4 | import { socketConnect } from 'socket.io-react'; 5 | import {cyan100, cyan500} from 'material-ui/styles/colors'; 6 | 7 | const styles = { 8 | nameStyle: { 9 | width: '50%', 10 | paddingLeft: 10, 11 | position: 'relative', 12 | top: -15 13 | }, 14 | underlineStyle: { 15 | borderColor: cyan500 16 | }, 17 | underlineFocusStyle: { 18 | borderColor: cyan100 19 | }, 20 | floatingLabelStyle: { 21 | color: cyan100 22 | } 23 | }; 24 | 25 | @ReactTimeout 26 | @socketConnect 27 | export default class Name extends React.Component { 28 | constructor() { 29 | super(); 30 | this.state = { 31 | disabled: false, 32 | floatingText: 'Press enter to submit' 33 | }; 34 | } 35 | 36 | handleChange(e) { 37 | sessionStorage.name = e.target.value; 38 | this.props.pushName(sessionStorage.name); 39 | } 40 | 41 | handleKeyPress(e) { 42 | // when press enter, we send the name with invalidate input 43 | if (e.key === 'Enter' && e.target.value) { 44 | this.setState({ 45 | disabled: true, 46 | floatingText: ' ' 47 | }); 48 | const self = this; 49 | this.props.setInterval( 50 | () => {self.props.socket.emit( 51 | 'is_online', 52 | self.props.name); 53 | }, 54 | 3000); 55 | } 56 | } 57 | 58 | render() { 59 | return( 60 |
61 | 62 | 76 |
77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /static/js/components/UserList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DropDownMenu from 'material-ui/DropDownMenu'; 3 | import MenuItem from "material-ui/MenuItem"; 4 | import { socketConnect } from 'socket.io-react'; 5 | 6 | import * as UsersListActions from '../actions/UsersListActions'; 7 | import UsersStore from '../stores/UsersStore'; 8 | 9 | const styles = { 10 | userListStyle: { 11 | width: '50%', 12 | position: 'relative', 13 | top: -15, 14 | right: 10 15 | }, 16 | dropDownStyles: { 17 | width: 240, 18 | top: 20 19 | } 20 | }; 21 | 22 | @socketConnect 23 | export default class UserList extends React.Component { 24 | constructor() { 25 | super(); 26 | this.state = { 27 | currentName: 'Select a user...', 28 | usersList: UsersStore.getAll() 29 | }; 30 | } 31 | 32 | handleChange = (event, index, value) => { 33 | this.setState({currentName: value}); 34 | if (value !== sessionStorage.name && value !== 'Select a user...') { 35 | this.props.socket.emit('build_private_room', { 36 | inviter: sessionStorage.name, 37 | guest: value 38 | }); 39 | } else { 40 | console.log('You can talk to yourself directly.'); 41 | } 42 | } 43 | 44 | componentWillMount() { 45 | UsersStore.on('updateUsersList', (invalidName) => { 46 | if (invalidName === this.state.currentName) { 47 | this.setState({ 48 | currentName: 'Select a user...', 49 | usersList: UsersStore.getAll() 50 | }); 51 | } else { 52 | this.setState({ 53 | usersList: UsersStore.getAll() 54 | }); 55 | } 56 | 57 | }); 58 | 59 | this.props.socket.on('connect', () => { 60 | UsersListActions.popName(); 61 | }); 62 | 63 | this.props.socket.on('online_name', (name) => { 64 | UsersListActions.pushName(name); 65 | }); 66 | 67 | } 68 | 69 | 70 | render() { 71 | const { usersList } = this.state; 72 | const self = this; 73 | const { socket } = self.props; 74 | const usernamesComponents = usersList.map((user) => { 75 | return ; 80 | }); 81 | 82 | return ( 83 |
84 | 85 | 91 | {usernamesComponents} 92 | 93 |
94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /venv/Include/site/python2.7/greenlet/greenlet.h: -------------------------------------------------------------------------------- 1 | /* vim:set noet ts=8 sw=8 : */ 2 | 3 | /* Greenlet object interface */ 4 | 5 | #ifndef Py_GREENLETOBJECT_H 6 | #define Py_GREENLETOBJECT_H 7 | 8 | #include 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | #define GREENLET_VERSION "0.4.10" 15 | 16 | typedef struct _greenlet { 17 | PyObject_HEAD 18 | char* stack_start; 19 | char* stack_stop; 20 | char* stack_copy; 21 | intptr_t stack_saved; 22 | struct _greenlet* stack_prev; 23 | struct _greenlet* parent; 24 | PyObject* run_info; 25 | struct _frame* top_frame; 26 | int recursion_depth; 27 | PyObject* weakreflist; 28 | PyObject* exc_type; 29 | PyObject* exc_value; 30 | PyObject* exc_traceback; 31 | PyObject* dict; 32 | } PyGreenlet; 33 | 34 | #define PyGreenlet_Check(op) PyObject_TypeCheck(op, &PyGreenlet_Type) 35 | #define PyGreenlet_MAIN(op) (((PyGreenlet*)(op))->stack_stop == (char*) -1) 36 | #define PyGreenlet_STARTED(op) (((PyGreenlet*)(op))->stack_stop != NULL) 37 | #define PyGreenlet_ACTIVE(op) (((PyGreenlet*)(op))->stack_start != NULL) 38 | #define PyGreenlet_GET_PARENT(op) (((PyGreenlet*)(op))->parent) 39 | 40 | #if (PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION >= 7) || (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 1) || PY_MAJOR_VERSION > 3 41 | #define GREENLET_USE_PYCAPSULE 42 | #endif 43 | 44 | /* C API functions */ 45 | 46 | /* Total number of symbols that are exported */ 47 | #define PyGreenlet_API_pointers 8 48 | 49 | #define PyGreenlet_Type_NUM 0 50 | #define PyExc_GreenletError_NUM 1 51 | #define PyExc_GreenletExit_NUM 2 52 | 53 | #define PyGreenlet_New_NUM 3 54 | #define PyGreenlet_GetCurrent_NUM 4 55 | #define PyGreenlet_Throw_NUM 5 56 | #define PyGreenlet_Switch_NUM 6 57 | #define PyGreenlet_SetParent_NUM 7 58 | 59 | #ifndef GREENLET_MODULE 60 | /* This section is used by modules that uses the greenlet C API */ 61 | static void **_PyGreenlet_API = NULL; 62 | 63 | #define PyGreenlet_Type (*(PyTypeObject *) _PyGreenlet_API[PyGreenlet_Type_NUM]) 64 | 65 | #define PyExc_GreenletError \ 66 | ((PyObject *) _PyGreenlet_API[PyExc_GreenletError_NUM]) 67 | 68 | #define PyExc_GreenletExit \ 69 | ((PyObject *) _PyGreenlet_API[PyExc_GreenletExit_NUM]) 70 | 71 | /* 72 | * PyGreenlet_New(PyObject *args) 73 | * 74 | * greenlet.greenlet(run, parent=None) 75 | */ 76 | #define PyGreenlet_New \ 77 | (* (PyGreenlet * (*)(PyObject *run, PyGreenlet *parent)) \ 78 | _PyGreenlet_API[PyGreenlet_New_NUM]) 79 | 80 | /* 81 | * PyGreenlet_GetCurrent(void) 82 | * 83 | * greenlet.getcurrent() 84 | */ 85 | #define PyGreenlet_GetCurrent \ 86 | (* (PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) 87 | 88 | /* 89 | * PyGreenlet_Throw( 90 | * PyGreenlet *greenlet, 91 | * PyObject *typ, 92 | * PyObject *val, 93 | * PyObject *tb) 94 | * 95 | * g.throw(...) 96 | */ 97 | #define PyGreenlet_Throw \ 98 | (* (PyObject * (*) \ 99 | (PyGreenlet *self, PyObject *typ, PyObject *val, PyObject *tb)) \ 100 | _PyGreenlet_API[PyGreenlet_Throw_NUM]) 101 | 102 | /* 103 | * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) 104 | * 105 | * g.switch(*args, **kwargs) 106 | */ 107 | #define PyGreenlet_Switch \ 108 | (* (PyObject * (*)(PyGreenlet *greenlet, PyObject *args, PyObject *kwargs)) \ 109 | _PyGreenlet_API[PyGreenlet_Switch_NUM]) 110 | 111 | /* 112 | * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) 113 | * 114 | * g.parent = new_parent 115 | */ 116 | #define PyGreenlet_SetParent \ 117 | (* (int (*)(PyGreenlet *greenlet, PyGreenlet *nparent)) \ 118 | _PyGreenlet_API[PyGreenlet_SetParent_NUM]) 119 | 120 | /* Macro that imports greenlet and initializes C API */ 121 | #ifdef GREENLET_USE_PYCAPSULE 122 | #define PyGreenlet_Import() \ 123 | { \ 124 | _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ 125 | } 126 | #else 127 | #define PyGreenlet_Import() \ 128 | { \ 129 | PyObject *module = PyImport_ImportModule("greenlet"); \ 130 | if (module != NULL) { \ 131 | PyObject *c_api_object = PyObject_GetAttrString( \ 132 | module, "_C_API"); \ 133 | if (c_api_object != NULL && PyCObject_Check(c_api_object)) { \ 134 | _PyGreenlet_API = \ 135 | (void **) PyCObject_AsVoidPtr(c_api_object); \ 136 | Py_DECREF(c_api_object); \ 137 | } \ 138 | Py_DECREF(module); \ 139 | } \ 140 | } 141 | #endif 142 | 143 | #endif /* GREENLET_MODULE */ 144 | 145 | #ifdef __cplusplus 146 | } 147 | #endif 148 | #endif /* !Py_GREENLETOBJECT_H */ 149 | -------------------------------------------------------------------------------- /static/js/components/Room.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { socketConnect } from 'socket.io-react'; 3 | import {Card, CardHeader} from 'material-ui/Card'; 4 | import TextField from 'material-ui/TextField'; 5 | import FlatButton from 'material-ui/FlatButton'; 6 | import IconButton from 'material-ui/IconButton'; 7 | import CloseButton from 'material-ui/svg-icons//navigation/close'; 8 | import Paper from 'material-ui/Paper'; 9 | import {cyan100, cyan500, grey100} from 'material-ui/styles/colors'; 10 | 11 | import RoomMsgStore from '../stores/RoomMsgStore'; 12 | import * as RoomMsgActions from '../actions/RoomMsgActions'; 13 | import * as RoomsActions from '../actions/RoomsActions'; 14 | 15 | const styles = { 16 | roomStyle: { 17 | width: '33.3%', 18 | display: 'inline-block', 19 | minWidth: 300, 20 | padding: 10 21 | }, 22 | paperStyle: { 23 | height: 400, 24 | position: 'relative', 25 | backgroundColor: grey100 26 | }, 27 | cardHeaderStyle: { 28 | backgroundColor: cyan100, 29 | paddingTop: 10, 30 | paddingBottom: 10 31 | }, 32 | closeStyle: { 33 | position: 'absolute', 34 | top: 0, 35 | right: 0 36 | }, 37 | chatRecord: { 38 | height: 290, 39 | padding: 10, 40 | backgroundColor: grey100, 41 | overflowY: 'auto', 42 | overflowX: 'hidden' 43 | }, 44 | singleRecord: { 45 | margin: '5px 0' 46 | }, 47 | inputArea: { 48 | position: 'absolute', 49 | bottom: 0, 50 | padding: '0 10px', 51 | width: 'calc(100% - 20px)' 52 | } 53 | }; 54 | 55 | @socketConnect 56 | export default class Room extends React.Component { 57 | constructor() { 58 | super(); 59 | this.state = { 60 | messages: RoomMsgStore.getAll() 61 | }; 62 | } 63 | 64 | handleKeyPress(e) { 65 | if (e.key === 'Enter' && e.target.value) { 66 | // console.log('I will send this message to the server.'); 67 | this.props.socket.emit('private_message', { 68 | msg: sessionStorage.name + ': ' + e.target.value, 69 | room: this.props.info.room 70 | }); 71 | e.target.value = ''; 72 | } 73 | } 74 | 75 | show() { 76 | const shouldShow = this.props.info.isActive; 77 | // console.log(shouldShow); 78 | if (shouldShow) { 79 | return ''; 80 | } else { 81 | return 'hide'; 82 | } 83 | } 84 | 85 | handleClose() { 86 | // console.log('We should make room[' + this.props.info.room + '] disappear.'); 87 | RoomsActions.updateStatus(this.props.info.room); 88 | } 89 | 90 | componentWillMount() { 91 | this.props.socket.on('room_message', (data) => { 92 | // console.log('I have received the message.'); 93 | if (this.props.info.room === data.room) { 94 | RoomMsgActions.sendMessage(data); 95 | } 96 | }); 97 | 98 | RoomMsgStore.on('pushNewMsg', (room) => { 99 | this.setState({ 100 | messages: RoomMsgStore.getAll() 101 | }); 102 | if (this.props.info.room === room) { 103 | const node = document.getElementById(room); 104 | node.scrollTop = node.scrollHeight; 105 | // console.log('Scroll bar has reached to the bottom!'); 106 | } 107 | }); 108 | } 109 | 110 | render() { 111 | const { messages } = this.state; 112 | const { info } = this.props; 113 | const oppositeName = (info.inviter === sessionStorage.name) ? 114 | info.guest : info.inviter; 115 | messages[info.room] = messages[info.room] || []; 116 | const msgComponent = messages[info.room].map((msg, index) => { 117 | return

{msg}

; 118 | }); 119 | 120 | return( 121 |
  • 122 | 123 | 124 | 129 | 132 | 133 | 134 |
    135 | {msgComponent} 136 |
    137 | 144 |
    145 |
    146 |
  • 147 | ); 148 | } 149 | } 150 | --------------------------------------------------------------------------------