├── .bowerrc ├── run.sh ├── dest.gif ├── output.gif ├── prod.yml ├── .gitignore ├── dev.yml ├── config ├── docker-local.js └── default.js ├── server ├── clientConfigParser.js ├── db.js └── index.js ├── bower.json ├── README.md ├── client ├── src │ ├── js │ │ ├── views │ │ │ ├── chat-view.js │ │ │ ├── message-collection-view.js │ │ │ └── new-message-form-view.js │ │ └── main.js │ └── scss │ │ └── main.scss ├── dist │ └── main.css └── index.html ├── Dockerfile ├── package.json └── gulpfile.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "client/lib/" 3 | } -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd /app && gulp && npm start -------------------------------------------------------------------------------- /dest.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thejsj/rethinkdb-chat/HEAD/dest.gif -------------------------------------------------------------------------------- /output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thejsj/rethinkdb-chat/HEAD/output.gif -------------------------------------------------------------------------------- /prod.yml: -------------------------------------------------------------------------------- 1 | server: 2 | build: . 3 | ports: 4 | - "10000:80" 5 | environment: 6 | NODE_ENV: production 7 | PORT: 80 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !* 2 | .DS_Store 3 | node_modules 4 | client/lib 5 | config 6 | fabfile.py 7 | *.pyc 8 | !config/default.js 9 | !config/docker-local.js 10 | *.mov 11 | title.txt -------------------------------------------------------------------------------- /dev.yml: -------------------------------------------------------------------------------- 1 | server: 2 | build: . 3 | # image: dockerfile/nodejs-bower-gulp-runtime 4 | ports: 5 | - "80:80" 6 | links: 7 | - "rethinkdb" 8 | environment: 9 | NODE_ENV: docker-local 10 | PORT: 80 11 | rethinkdb: 12 | image: dockerfile/rethinkdb 13 | ports: 14 | - "10001:8080" 15 | - "10002:28015" 16 | - "10003:29015" -------------------------------------------------------------------------------- /config/docker-local.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 'use strict'; 3 | /** 4 | * Inherits from `default.js` 5 | */ 6 | var config = { 7 | 'rethinkdb': { 8 | 'host': process.env.RETHINKDB_PORT_28015_TCP_ADDR, 9 | 'port': process.env.RETHINKDB_PORT_28015_TCP_PORT 10 | }, 11 | 'ports' : { 12 | 'http' : process.env.PORT 13 | }, 14 | 'url': 'docker.dev' 15 | }; 16 | 17 | module.exports = config; -------------------------------------------------------------------------------- /server/clientConfigParser.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 'use strict'; 3 | var config = require('config'); 4 | 5 | var clientConfigParser = function (req, res) { 6 | var _config = { 7 | 'ports': config.get('ports'), 8 | 'url': config.get('url') 9 | }; 10 | var str = 'window.config = ' + JSON.stringify(_config) + ';'; 11 | res 12 | .type('text/javascript') 13 | .send(str); 14 | }; 15 | 16 | module.exports = clientConfigParser; -------------------------------------------------------------------------------- /config/default.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 'use strict'; 3 | /** 4 | * Configuration Structure 5 | * 6 | * default.js 7 | * - test.js 8 | * - development.js 9 | * - - staging.js 10 | * - - - production.js 11 | */ 12 | var config = { 13 | 'rethinkdb': { 14 | 'host': 'localhost', 15 | 'port': 28015, 16 | 'db': 'rethink_chat' 17 | }, 18 | 'ports' : { 19 | 'http' : 8000 20 | }, 21 | 'url': '127.0.0.1', 22 | }; 23 | 24 | module.exports = config; -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 'use strict'; 3 | 4 | var config = require('config'); 5 | var q = require('q'); 6 | var r = require('rethinkdb'); 7 | require('rethinkdb-init')(r); 8 | 9 | // Create Tables 10 | r.init(config.get('rethinkdb'), [ 11 | { 12 | name: 'messages', 13 | indexes: ['created'] 14 | }, 15 | { 16 | name: 'users' 17 | } 18 | ]) 19 | .then(function (conn) { 20 | r.conn = conn; 21 | r.conn.use(config.get('rethinkdb').db); 22 | }); 23 | 24 | module.exports = r; 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rethink-chat", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/thejsj/dotfiles", 5 | "authors": [ 6 | "thejsj " 7 | ], 8 | "license": "MIT", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "client/lib/", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "bootstrap-sass-official": "~3.3.3", 19 | "bootstrap": "~3.3.2", 20 | "socket.io": "~1.3.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # RethinkDB Chat 3 | 4 | ![](output.gif) 5 | 6 | Dead simple, realtime chat app built with RethinkDB and React.js. 7 | 8 | ## Setup 9 | 10 | Install RethinkDB 11 | 12 | ``` 13 | brew install rethinkdb 14 | ``` 15 | 16 | Install gulp 17 | 18 | ``` 19 | npm install -g gulp 20 | ``` 21 | 22 | Install node dependencies 23 | ``` 24 | npm install 25 | ``` 26 | 27 | ## Running 28 | 29 | Run node server 30 | 31 | ``` 32 | npm run dev 33 | ``` 34 | 35 | Compile client side assets 36 | 37 | ``` 38 | gulp 39 | ``` 40 | -------------------------------------------------------------------------------- /client/src/js/views/chat-view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var React = require('react'); 3 | var MessageCollectionView = require('./message-collection-view'); 4 | var NewMessageFormView = require('./new-message-form-view'); 5 | 6 | var ChatView = React.createClass({ 7 | render: function () { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | }); 16 | 17 | module.exports = ChatView; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Node.js runtime Dockerfile 3 | # 4 | # https://github.com/dockerfile/nodejs-runtime 5 | # 6 | 7 | # Pull base image. 8 | FROM node 9 | 10 | # Set instructions on build. 11 | RUN npm install -g gulp bower node-sass 12 | RUN npm cache clean 13 | 14 | # Define working directory. 15 | WORKDIR /app 16 | ADD ./package.json /app/package.json 17 | RUN npm install 18 | 19 | WORKDIR / 20 | ADD . ./app 21 | 22 | WORKDIR /app 23 | RUN bower install --allow-root 24 | ADD run.sh /run.sh 25 | RUN chmod -R 777 /run.sh 26 | RUN chmod +x /run.sh 27 | 28 | # Expose ports. 29 | EXPOSE 80 30 | 31 | WORKDIR /app 32 | ENTRYPOINT ["/run.sh"] 33 | -------------------------------------------------------------------------------- /client/dist/main.css: -------------------------------------------------------------------------------- 1 | #container { 2 | position: relative; 3 | height: 100vh; 4 | overflow: hidden; } 5 | #container #message-form { 6 | position: absolute; 7 | bottom: 0; 8 | width: 100%; 9 | padding-top: 5px; 10 | padding-bottom: 10px; 11 | background: white; 12 | height: 40px; 13 | border-top: solid 1px #f2f2f2; } 14 | #container #message-form input[type='text'] { 15 | margin-bottom: 10px; 16 | width: calc(100% - 100px); } 17 | #container #message-form input[type='submit'] { 18 | width: 100px; 19 | float: right; } 20 | #container .message-collection-container { 21 | padding-top: 10px; 22 | height: calc(100vh - 40px); 23 | overflow-y: scroll; } 24 | -------------------------------------------------------------------------------- /client/src/js/main.js: -------------------------------------------------------------------------------- 1 | /*global io:true, $:true, console:true */ 2 | 'use strict'; 3 | var React = require('react'); 4 | 5 | var socket = io.connect('http://' + window.config.url + ':' + window.config.ports.http); 6 | var messageCollection = []; 7 | var ChatView = require('./views/chat-view'); 8 | 9 | var userName = prompt('Pick a username'); 10 | 11 | var render = function () { 12 | React.render( 13 | , 14 | document.getElementById('container') 15 | ); 16 | }; 17 | render(); 18 | 19 | socket.on('message', function (message) { 20 | console.log('Message', message); 21 | messageCollection.push(message); 22 | render(); 23 | }); 24 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /client/src/scss/main.scss: -------------------------------------------------------------------------------- 1 | 2 | $chat-box-height: 40px; 3 | 4 | #container { 5 | position: relative; 6 | height: 100vh; 7 | overflow: hidden; 8 | 9 | #message-form { 10 | position: absolute; 11 | bottom: 0; 12 | width: 100%; 13 | padding-top: 5px; 14 | padding-bottom: 10px; 15 | background: white; 16 | height: $chat-box-height; 17 | border-top: solid 1px #f2f2f2; 18 | $submit-button-width: 100px; 19 | 20 | input[type='text'] { 21 | margin-bottom: 10px; 22 | width: calc(100% - #{$submit-button-width}); 23 | } 24 | 25 | input[type='submit'] { 26 | width: $submit-button-width; 27 | float: right; 28 | } 29 | } 30 | 31 | .message-collection-container { 32 | padding-top: 10px; 33 | height: calc(100vh - #{$chat-box-height}); 34 | overflow-y: scroll; 35 | } 36 | } -------------------------------------------------------------------------------- /client/src/js/views/message-collection-view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var React = require('react'); 3 | var _ = require('lodash'); 4 | 5 | var MessageCollectionView = React.createClass({ 6 | render: function() { 7 | var messageCollection = this.props.messageCollection; 8 | setTimeout(function () { 9 | // I'm on the train and can't google a better way to do this.... 10 | var div = document.querySelector('.message-collection-container'); 11 | div.scrollTop = Infinity; 12 | }); 13 | return ( 14 |
15 | {messageCollection.map(function(item, i) { 16 | return ( 17 |
18 |
19 |

{ item.user }: { item.message }

20 |
21 |
22 | ); 23 | }, this)} 24 |
25 | ); 26 | } 27 | }); 28 | 29 | module.exports = MessageCollectionView; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rethink-chat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "./node_modules/nodemon/bin/nodemon.js --watch ./server server", 9 | "start": "node server" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "body-parser": "^1.11.0", 15 | "config": "^1.12.0", 16 | "express": "^4.11.2", 17 | "lodash": "^3.3.0", 18 | "q": "^1.1.2", 19 | "react": "^0.12.2", 20 | "rethinkdb": "^1.16.0", 21 | "rethinkdb-init": "0.0.2", 22 | "socket.io": "^1.3.4" 23 | }, 24 | "devDependencies": { 25 | "browserify": "^8.1.3", 26 | "gulp": "^3.8.11", 27 | "gulp-concat": "^2.4.3", 28 | "gulp-sass": "^1.3.3", 29 | "gulp-streamify": "0.0.5", 30 | "gulp-uglify": "^1.1.0", 31 | "nodemon": "^1.3.7", 32 | "reactify": "^1.0.0", 33 | "vinyl-source-stream": "^1.0.0", 34 | "watchify": "^2.3.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/src/js/views/new-message-form-view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var React = require('react'); 3 | 4 | var NewMessageFormView = React.createClass({ 5 | getInitialState: function() { 6 | return {value: ''}; 7 | }, 8 | handleChange: function () { 9 | this.setState({value: event.target.value}); 10 | }, 11 | handleSubmit: function (e) { 12 | e.preventDefault(); 13 | var text = this.state.value; 14 | console.log(text); 15 | this.props.socket.emit('message', { 16 | message: text, 17 | userName: this.props.userName 18 | }); 19 | this.state.value = ''; 20 | }, 21 | render: function () { 22 | var value = this.state.value; 23 | return ( 24 |
25 |
26 | 27 | 28 |
29 |
30 | ); 31 | } 32 | }); 33 | 34 | module.exports = NewMessageFormView; 35 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 'use strict'; 3 | 4 | var config = require('config'); 5 | var express = require('express'); 6 | var app = express(); 7 | var server = require('http').Server(app); 8 | var io = require('socket.io')(server); 9 | var r = require('./db'); 10 | var clientConfigParser = require('./clientConfigParser'); 11 | 12 | console.log('config'); 13 | console.log(config); 14 | 15 | server.listen(config.get('ports').http); 16 | 17 | // Static Dirname 18 | app 19 | .use('/config.js', clientConfigParser) 20 | .use(express.static(__dirname + '/../client')); 21 | 22 | io.on('connection', function (socket) { 23 | 24 | r.table('messages') 25 | .orderBy({index: 'created'}) 26 | .coerceTo('array') 27 | .run(r.conn) 28 | .then(function (messages) { 29 | messages.forEach(function (message) { 30 | socket.emit('message', message); 31 | }); 32 | }); 33 | 34 | // Listen to new message being inserted 35 | r.connect(config.get('rethinkdb')) 36 | .then(function (conn) { 37 | r.table('messages') 38 | .changes().run(conn) 39 | .then(function(cursor) { 40 | cursor.each(function (err, row) { 41 | socket.emit('message', row.new_val); 42 | }, function () { 43 | console.log('Finished'); 44 | }); 45 | }); 46 | }); 47 | 48 | // Insert new messages 49 | socket.on('message', function (data) { 50 | r.table('messages').insert({ 51 | message: data.message, 52 | user: data.userName, 53 | created: (new Date()).getTime() 54 | }).run(r.conn); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 'use strict'; 3 | 4 | var gulp = require('gulp'); 5 | var browserify = require('browserify'); 6 | var watchify = require('watchify'); 7 | var source = require('vinyl-source-stream'); 8 | var reactify = require('reactify'); 9 | var uglify = require('gulp-uglify'); 10 | var streamify = require('gulp-streamify'); 11 | 12 | // var sass = require('gulp-sass'); 13 | var concat = require('gulp-concat'); 14 | 15 | gulp.task('watchify', function(){ 16 | var bundler = browserify({ 17 | entries: ['./client/src/js/main.js'], // Only need initial file, browserify finds the deps 18 | transform: [reactify], // We want to convert JSX to normal javascript 19 | cache: {}, packageCache: {}, fullPaths: true // Requirement of watchify 20 | }); 21 | var watcher = watchify(bundler); 22 | return watcher 23 | .on('update', function () { // When any files update 24 | var updateStart = Date.now(); 25 | watcher.bundle() // Create new bundle that uses the cache for high performance 26 | .pipe(source('main.js')) 27 | // .pipe(streamify(uglify())) 28 | .pipe(gulp.dest('./client/dist/')); 29 | console.log('Updated!', (Date.now() - updateStart) + 'ms'); 30 | }) 31 | .bundle() // Create the initial bundle when starting the task 32 | .pipe(source('main.js')) 33 | // .pipe(streamify(uglify())) 34 | .pipe(gulp.dest('./client/dist/')); 35 | }); 36 | 37 | gulp.task('browserify', function(){ 38 | var bundler = browserify({ 39 | entries: ['./client/src/js/main.js'], // Only need initial file, browserify finds the deps 40 | transform: [reactify], // We want to convert JSX to normal javascript 41 | cache: {}, packageCache: {}, fullPaths: true // Requirement of watchify 42 | }); 43 | return bundler 44 | .bundle() // Create the initial bundle when starting the task 45 | .pipe(source('main.js')) 46 | // .pipe(streamify(uglify())) 47 | .pipe(gulp.dest('./client/dist/')); 48 | }); 49 | 50 | //gulp.task('sass', function () { 51 | //gulp.src([ 52 | //'client/src/scss/main.scss' 53 | //]) 54 | //.pipe(sass({ 55 | //errLogToConsole: false 56 | //})) 57 | //.pipe(concat('main.css')) 58 | //.pipe(gulp.dest('./client/dist/')); 59 | //}); 60 | 61 | gulp.task('watch', ['browserify'], function () { 62 | // gulp.watch('./client/src/scss/**/*.scss', ['sass']); 63 | gulp.watch('./client/src/js/**/*.js', ['watchify']); 64 | }); 65 | 66 | gulp.task('default', ['browserify' ]); 67 | --------------------------------------------------------------------------------