├── .gitignore ├── .jshintrc ├── LICENSE ├── Procfile ├── README.md ├── lib ├── faye.js └── gitter.js ├── package.json ├── public ├── app.less ├── build │ ├── app.css │ ├── app.css.map │ ├── app.js │ └── app.js.map ├── bundle.js ├── bundle.js.map ├── components │ ├── app.jsx │ ├── input.jsx │ ├── messages.jsx │ └── nav.jsx ├── favicon.ico ├── main.jsx └── reset.css ├── router ├── home.js ├── msg-stream.js ├── oauth.js ├── render-room.js └── send.js ├── server.js ├── views ├── index.ejs └── login.ejs └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "node": true, 4 | "unused": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mauro Pompilio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitter React 2 | 3 | [![Join the chat at https://gitter.im/malditogeek/gitter-react](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/malditogeek/gitter-react?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | A mobile Gitter client using isomorphic React. 6 | 7 | You can try it here: http://gitter-react.herokuapp.com 8 | 9 | ## WIP 10 | 11 | I'm building this as an exercise to learn some React. Feel free to contribute tho :+1: 12 | 13 | ## Getting Started 14 | 15 | You'll need to create an app at https://developer.gitter.im and expose your credentials through env vars: 16 | 17 | - `export OAUTH_KEY=` 18 | - `export OAUTH_SECRET=` 19 | - `export REDIRECT=` 20 | - `export SESSION_SECRET=` 21 | 22 | - Start the server: `npm start` 23 | - Build the app+css: `npm run webpack` 24 | 25 | ## License 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /lib/faye.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, unused:true */ 2 | 3 | var Faye = require('gitter-faye'); 4 | //var EventEmitter = require('eventemitter3'); 5 | 6 | // Authentication extension for Faye 7 | var ClientAuthExt = function(opts) { 8 | this.token = opts.token; 9 | this.clientType = opts.clientType; 10 | }; 11 | 12 | ClientAuthExt.prototype.outgoing = function(message, callback) { 13 | if (message.channel == '/meta/handshake') { 14 | if (!message.ext) message.ext = {}; 15 | if (this.clientType) message.ext.client = this.clientType; 16 | message.ext.token = this.token; 17 | } 18 | 19 | callback(message); 20 | }; 21 | 22 | // Snapshot extension for Faye 23 | var SnapshotExt = function(opts) { 24 | this.subscriptions = opts.subscriptions; 25 | }; 26 | 27 | SnapshotExt.prototype.incoming = function(message, callback) { 28 | if(message.channel == '/meta/subscribe' && message.ext && message.ext.snapshot) { 29 | var sub = this.subscriptions[message.subscription]; 30 | if (sub) sub.emitter.emit('snapshot', message.ext.snapshot); 31 | } 32 | 33 | callback(message); 34 | }; 35 | 36 | // Client wrapper 37 | var FayeClient = function(token, opts) { 38 | opts = opts || {}; 39 | var host = opts.host || 'https://ws.gitter.im/faye'; 40 | 41 | this.subscriptions = {}; 42 | 43 | this.client = new Faye.Client(host, {timeout: 60, retry: 5, interval: 1}); 44 | this.client.addExtension(new ClientAuthExt({token: token, clientType: opts.clientType})); 45 | this.client.addExtension(new SnapshotExt({subscriptions: this.subscriptions})); 46 | }; 47 | 48 | //FayeClient.prototype.subscribeTo = function(resource, eventName) { 49 | // if (this.subscriptions[resource]) return this.subscriptions[resource].emitter; 50 | // 51 | // var emitter = new EventEmitter(); 52 | // var subscription = this.client.subscribe(resource, function(msg) { 53 | // emitter.emit(eventName, msg); 54 | // }); 55 | // 56 | // this.subscriptions[resource] = { 57 | // eventName: eventName, 58 | // emitter: emitter, 59 | // subscription: subscription 60 | // }; 61 | // 62 | // return emitter; 63 | //}; 64 | 65 | FayeClient.prototype.disconnect = function() { 66 | var self = this; 67 | 68 | Object.keys(this.subscriptions).forEach(function(sub) { 69 | self.subscriptions[sub].subscription.cancel(); 70 | self.subscriptions[sub].emitter.removeAllListeners(); 71 | }); 72 | }; 73 | 74 | module.exports = FayeClient; 75 | -------------------------------------------------------------------------------- /lib/gitter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('gitter'); 4 | var https = require('https'); 5 | var qs = require('qs'); 6 | 7 | var Client = function(token) { 8 | this.token = token; 9 | }; 10 | 11 | ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach(function(method) { 12 | Client.prototype[method.toLowerCase()] = function(path, query, body) { 13 | return this.request(method, path, query, body); 14 | }; 15 | }); 16 | 17 | Client.prototype.request = function(method, path, query, body) { 18 | debug(method, path, query, body); 19 | 20 | path = encodeURI(path); 21 | 22 | var headers = { 23 | 'User-Agent': 'Gitter Client', 24 | 'Accept' : 'application/json' 25 | }; 26 | 27 | if (this.token) { 28 | headers.Authorization = `Bearer ${this.token}`; 29 | } else { 30 | query = query || {}; 31 | query.client_id = process.env.GHCLIENT; 32 | query.client_secret = process.env.GHSECRET; 33 | } 34 | 35 | if (body) headers['Content-Type'] = 'application/json'; 36 | 37 | var options = { 38 | hostname: 'api.gitter.im', 39 | port: 443, 40 | method: method, 41 | path: query ? path + '?' + qs.stringify(query) : path, 42 | headers: headers 43 | }; 44 | 45 | return new Promise(function(resolve, reject) { 46 | var responseHandler = function(res) { 47 | res.setEncoding('utf8'); 48 | 49 | var data = ''; 50 | res.on('data', chunk => data += chunk ); 51 | 52 | res.on('end', () => { 53 | debug(res.statusCode + ' ' + method + ' ' + options.path); 54 | try { resolve(JSON.parse(data)); } catch(err) { reject('Invalid JSON'); } 55 | }); 56 | 57 | 58 | if ([200,201].indexOf(res.statusCode) === -1) { 59 | return reject(new Error(res.statusCode)); 60 | } 61 | }; 62 | 63 | var req = https.request(options, responseHandler); 64 | req.on('error', reject); 65 | if (body) req.write(JSON.stringify(body)); 66 | req.end(); 67 | }); 68 | }; 69 | 70 | module.exports = Client; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitter-react", 3 | "version": "0.1.0", 4 | "description": "Isomorphic React Gitter Client", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "webpack": "webpack --progress --colors --watch -d", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "dependencies": { 14 | "babel": "^5.2.16", 15 | "body-parser": "^1.12.3", 16 | "compression": "^1.4.3", 17 | "cookie-session": "^1.1.0", 18 | "css-loader": "^0.12.0", 19 | "debug": "^2.1.3", 20 | "ejs": "^2.3.1", 21 | "express": "^4.12.3", 22 | "extract-text-webpack-plugin": "^0.8.0", 23 | "gitter-faye": "^1.1.0-h", 24 | "jsx-loader": "^0.13.2", 25 | "less": "^2.5.0", 26 | "less-loader": "^2.2.0", 27 | "oauth": "^0.9.12", 28 | "oauth2": "0.0.1", 29 | "qs": "^2.4.1", 30 | "react": "^0.13.2", 31 | "react-tools": "^0.13.2", 32 | "serve-static": "^1.9.2", 33 | "style-loader": "^0.12.1", 34 | "superagent": "^1.2.0", 35 | "underscore": "^1.8.3" 36 | }, 37 | "devDependencies": { 38 | "nodemon": "^1.3.7", 39 | "webpack": "^1.9.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/app.less: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Source Sans Pro', sans-serif !important; 3 | font-size: 16px !important; 4 | color: #333; 5 | } 6 | 7 | img { 8 | max-width: 100%; 9 | } 10 | 11 | 12 | .nav { 13 | .btn { 14 | background: white; 15 | padding: 10px; 16 | border-radius: 50px; 17 | cursor: pointer; 18 | font-size: 1.5em; 19 | } 20 | 21 | .menu { 22 | position: fixed; 23 | top: 10px; 24 | right: 10px; 25 | } 26 | 27 | h1 { 28 | position: fixed; 29 | top: 10px; 30 | left: 0; 31 | right: 0; 32 | margin: 0 auto; 33 | padding: 10px; 34 | width: 150px; 35 | color: white; 36 | font-weight: bold; 37 | background: #333; 38 | border-radius: 5px; 39 | text-align: center; 40 | } 41 | } 42 | 43 | .messages { 44 | padding-bottom: 60px; 45 | } 46 | 47 | .message { 48 | margin-bottom: 10px; 49 | 50 | .avatar { 51 | width: 40px; 52 | height: 40px; 53 | border-radius: 50px; 54 | margin: 0px 10px; 55 | float: left; 56 | } 57 | 58 | .html { 59 | float: left; 60 | margin-top: 10px; 61 | max-width: 80%; 62 | word-wrap: break-word; 63 | } 64 | 65 | .clear { 66 | clear: both; 67 | } 68 | } 69 | 70 | .hidden-menu, .hidden { 71 | display: none; 72 | } 73 | 74 | .visible { 75 | display: block; 76 | } 77 | 78 | .visible-menu { 79 | cursor: pointer; 80 | position: fixed; 81 | background: #333; 82 | opacity: 0.95; 83 | top: 0px; 84 | left: 0px; 85 | width: 100%; 86 | height: 100%; 87 | overflow-y: scroll; 88 | z-index: 2; 89 | } 90 | 91 | .roster-item { 92 | margin: 20px 10px; 93 | a { 94 | text-decoration: none; 95 | font-weight: bold; 96 | font-size: 1.2em; 97 | color: white; 98 | } 99 | } 100 | 101 | .input-textarea { 102 | position: fixed; 103 | bottom: 10px; 104 | left: 10px; 105 | width: 250px; 106 | border: 1px solid #CCC; 107 | border-radius: 5px; 108 | font-size: 1em; 109 | box-shadow: none !important; 110 | -webkit-appearance: none; 111 | } 112 | 113 | .input-submit { 114 | position: fixed; 115 | bottom: 5px; 116 | right: 10px; 117 | font-size: 1.5em; 118 | padding: 15px; 119 | cursor: pointer; 120 | } 121 | -------------------------------------------------------------------------------- /public/build/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Source Sans Pro', sans-serif !important; 3 | font-size: 16px !important; 4 | color: #333; 5 | } 6 | img { 7 | max-width: 100%; 8 | } 9 | .nav .btn { 10 | background: white; 11 | padding: 10px; 12 | border-radius: 50px; 13 | cursor: pointer; 14 | font-size: 1.5em; 15 | } 16 | .nav .menu { 17 | position: fixed; 18 | top: 10px; 19 | right: 10px; 20 | } 21 | .nav h1 { 22 | position: fixed; 23 | top: 10px; 24 | left: 0; 25 | right: 0; 26 | margin: 0 auto; 27 | padding: 10px; 28 | width: 150px; 29 | color: white; 30 | font-weight: bold; 31 | background: #333; 32 | border-radius: 5px; 33 | text-align: center; 34 | } 35 | .messages { 36 | padding-bottom: 60px; 37 | } 38 | .message { 39 | margin-bottom: 10px; 40 | } 41 | .message .avatar { 42 | width: 40px; 43 | height: 40px; 44 | border-radius: 50px; 45 | margin: 0px 10px; 46 | float: left; 47 | } 48 | .message .html { 49 | float: left; 50 | margin-top: 10px; 51 | max-width: 80%; 52 | word-wrap: break-word; 53 | } 54 | .message .clear { 55 | clear: both; 56 | } 57 | .hidden-menu, 58 | .hidden { 59 | display: none; 60 | } 61 | .visible { 62 | display: block; 63 | } 64 | .visible-menu { 65 | cursor: pointer; 66 | position: fixed; 67 | background: #333; 68 | opacity: 0.95; 69 | top: 0px; 70 | left: 0px; 71 | width: 100%; 72 | height: 100%; 73 | overflow-y: scroll; 74 | z-index: 2; 75 | } 76 | .roster-item { 77 | margin: 20px 10px; 78 | } 79 | .roster-item a { 80 | text-decoration: none; 81 | font-weight: bold; 82 | font-size: 1.2em; 83 | color: white; 84 | } 85 | .input-textarea { 86 | position: fixed; 87 | bottom: 10px; 88 | left: 10px; 89 | width: 250px; 90 | border: 1px solid #CCC; 91 | border-radius: 5px; 92 | font-size: 1em; 93 | box-shadow: none !important; 94 | -webkit-appearance: none; 95 | } 96 | .input-submit { 97 | position: fixed; 98 | bottom: 5px; 99 | right: 10px; 100 | font-size: 1.5em; 101 | padding: 15px; 102 | cursor: pointer; 103 | } 104 | html, body, div, span, applet, object, iframe, 105 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 106 | a, abbr, acronym, address, big, cite, code, 107 | del, dfn, em, img, ins, kbd, q, s, samp, 108 | small, strike, strong, sub, sup, tt, var, 109 | b, u, i, center, 110 | dl, dt, dd, ol, ul, li, 111 | fieldset, form, label, legend, 112 | table, caption, tbody, tfoot, thead, tr, th, td, 113 | article, aside, canvas, details, embed, 114 | figure, figcaption, footer, header, hgroup, 115 | menu, nav, output, ruby, section, summary, 116 | time, mark, audio, video { 117 | margin: 0; 118 | padding: 0; 119 | border: 0; 120 | font-size: 100%; 121 | font: inherit; 122 | vertical-align: baseline; 123 | } 124 | /* HTML5 display-role reset for older browsers */ 125 | article, aside, details, figcaption, figure, 126 | footer, header, hgroup, menu, nav, section { 127 | display: block; 128 | } 129 | body { 130 | line-height: 1; 131 | } 132 | ol, ul { 133 | list-style: none; 134 | } 135 | blockquote, q { 136 | quotes: none; 137 | } 138 | blockquote:before, blockquote:after, 139 | q:before, q:after { 140 | content: ''; 141 | content: none; 142 | } 143 | table { 144 | border-collapse: collapse; 145 | border-spacing: 0; 146 | } 147 | 148 | /*# sourceMappingURL=app.css.map*/ -------------------------------------------------------------------------------- /public/build/app.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"./public/build/app.css","sourceRoot":""} -------------------------------------------------------------------------------- /public/components/app.jsx: -------------------------------------------------------------------------------- 1 | 2 | var React = require('react/addons'); 3 | 4 | var Nav = require('./nav'); 5 | var Messages = require('./messages'); 6 | var Input = require('./input'); 7 | 8 | class ReactApp extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | seed: props.seed 14 | }; 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |
24 | ) 25 | } 26 | } 27 | 28 | module.exports = ReactApp; 29 | -------------------------------------------------------------------------------- /public/components/input.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react/addons'); 2 | var agent = require('superagent'); 3 | 4 | class Input extends React.Component { 5 | submit(evt) { 6 | evt.stopPropagation(); 7 | evt.preventDefault(); 8 | 9 | var input = document.getElementById('input'); 10 | var text = input.value; 11 | if (text === '') return; 12 | 13 | agent 14 | .post('/send') 15 | .send({room: this.props.room.id, text: text}) 16 | .end( (err, res) => { 17 | if (res.ok) { 18 | input.value = ''; 19 | } else { 20 | alert('Try again!'); 21 | } 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 | 29 | 30 |
31 | ) 32 | } 33 | } 34 | 35 | module.exports = Input; 36 | -------------------------------------------------------------------------------- /public/components/messages.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | class Message extends React.Component { 6 | html() { 7 | return {__html: this.props.html}; 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 | 14 |
15 |
16 |
17 | ) 18 | } 19 | } 20 | 21 | class Messages extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | 25 | this.state = { 26 | data: props.data, 27 | room: props.room 28 | } 29 | } 30 | 31 | scroll() { 32 | var el = document.getElementById('messages'); 33 | window.scrollTo(0, el.scrollHeight); 34 | } 35 | 36 | componentDidMount() { 37 | var evtSource = new EventSource(`/rooms/${this.state.room.id}/stream`); 38 | evtSource.addEventListener('message', evt => { 39 | var msg = JSON.parse(evt.data); 40 | 41 | if (msg.operation === 'create') { 42 | this.state.data.shift(); 43 | this.setState({ 44 | date: this.state.data.push(msg.model) 45 | }) 46 | }; 47 | }); 48 | 49 | this.scroll(); 50 | } 51 | 52 | componentDidUpdate() { 53 | this.scroll(); 54 | } 55 | 56 | render() { 57 | var messageNodes = this.state.data.map(function(msg, index) { 58 | return ( 59 | 60 | ); 61 | }); 62 | return ( 63 |
64 | {messageNodes} 65 |
66 | ) 67 | } 68 | } 69 | 70 | module.exports = Messages; 71 | -------------------------------------------------------------------------------- /public/components/nav.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | class RosterItem extends React.Component { 6 | render() { 7 | return ( 8 | 11 | ) 12 | } 13 | } 14 | 15 | class Roster extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | menu: 'hidden-menu', 21 | }; 22 | } 23 | 24 | showMenu(evt) { 25 | evt.stopPropagation(); 26 | evt.preventDefault(); 27 | 28 | this.setState({ 29 | menu: 'visible-menu', 30 | }); 31 | } 32 | 33 | hideMenu() { 34 | this.setState({ 35 | menu: 'hidden-menu', 36 | }); 37 | } 38 | 39 | render() { 40 | var rosterItems = this.props.rooms.map( (room, index) => { 41 | return ( 42 | 43 | ); 44 | }); 45 | return ( 46 |
47 | 48 |
49 | {rosterItems} 50 |
51 |
52 | ) 53 | } 54 | } 55 | 56 | class Nav extends React.Component { 57 | render() { 58 | return ( 59 |
60 |

{this.props.room.uri}

61 | 62 |
63 | ) 64 | } 65 | } 66 | 67 | module.exports = Nav; 68 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malditogeek/gitter-react/99662bcb2f1e9c60d65467f438c5773733d7eb20/public/favicon.ico -------------------------------------------------------------------------------- /public/main.jsx: -------------------------------------------------------------------------------- 1 | require('./app.less'); 2 | require('./reset.css'); 3 | 4 | var React = require('react/addons'); 5 | var ReactApp = React.createFactory(require('./components/app')); 6 | 7 | var mountNode = document.getElementById('react-app'); 8 | React.render(new ReactApp({seed: window.seed}), mountNode); 9 | -------------------------------------------------------------------------------- /public/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | -------------------------------------------------------------------------------- /router/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | var Gitter = require('../lib/gitter'); 5 | 6 | var home = (req, res) => { 7 | if (!req.session.token) return res.render('login'); 8 | 9 | var gitter = new Gitter(req.session.token); 10 | gitter.get('/v1/rooms') 11 | .then( (_rooms) => { 12 | var allRooms = _rooms.filter( r => r.lastAccessTime ); 13 | var sortedRooms = _.sortBy(allRooms, 'lastAccessTime').reverse(); 14 | var people = sortedRooms.filter( r => r.oneToOne ).slice(0,10); 15 | var rooms = sortedRooms.filter( r => !r.oneToOne ).slice(0,10); 16 | 17 | res.redirect(`/rooms/${rooms[0].id}`); 18 | }) 19 | .catch( err => { 20 | console.log('ERR', err); 21 | res.status(500).send('Something went wrong'); 22 | }); 23 | }; 24 | 25 | module.exports = home; 26 | -------------------------------------------------------------------------------- /router/msg-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fayeWrapper = require('../lib/faye'); 4 | 5 | var stream = (req, res) => { 6 | res.writeHead(200, { 7 | 'Content-Type': 'text/event-stream', 8 | 'Cache-Control': 'no-cache', 9 | 'Connection': 'keep-alive', 10 | 'Access-Control-Allow-Origin': '*' 11 | }); 12 | 13 | // Heartbeat 14 | var nln = () => res.write('\n'); 15 | var hbt = setInterval(nln, 15000); 16 | 17 | // Retry 18 | res.write("retry: 500\n"); 19 | 20 | var publish = (msg) => { 21 | res.write(`event: message\n`); 22 | res.write(`data: ${JSON.stringify(msg)}\n\n`); 23 | res.flush(); 24 | }; 25 | 26 | var roomId= req.params.roomId; 27 | var faye = new fayeWrapper(req.session.token); 28 | faye.client.subscribe(`/api/v1/rooms/${roomId}/chatMessages`, publish); 29 | 30 | // Clear heartbeat and listener 31 | req.on('close', () => { 32 | clearInterval(hbt); 33 | faye.client.disconnect(); 34 | }); 35 | }; 36 | 37 | module.exports = stream; 38 | -------------------------------------------------------------------------------- /router/oauth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var OAuth2 = require('oauth').OAuth2; 5 | 6 | var OAUTH_KEY = process.env.OAUTH_KEY; 7 | var OAUTH_SECRET = process.env.OAUTH_SECRET; 8 | var BASEPATH = 'https://gitter.im/'; 9 | var REDIRECT = process.env.REDIRECT; 10 | 11 | var auth = new OAuth2(OAUTH_KEY, OAUTH_SECRET, BASEPATH, 'login/oauth/authorize', 'login/oauth/token'); 12 | 13 | var login = (req, res) => { 14 | var url = auth.getAuthorizeUrl({ 15 | redirect_uri: REDIRECT, 16 | response_type: 'code' 17 | }); 18 | res.redirect(url); 19 | }; 20 | 21 | var callback = (req, res) => { 22 | var code = req.query.code; 23 | var params = { 24 | redirect_uri: REDIRECT, 25 | grant_type: 'authorization_code' 26 | }; 27 | 28 | auth.getOAuthAccessToken(code, params, (err, access_token) => { 29 | req.session.token = access_token; 30 | res.redirect('/'); 31 | }); 32 | }; 33 | 34 | var logout = (req, res) => { 35 | req.session.destroy(); 36 | res.redirect('/'); 37 | }; 38 | 39 | var app = express(); 40 | 41 | app.get('/login', login); 42 | app.get('/callback', callback); 43 | app.get('/logout', logout); 44 | 45 | module.exports = app; 46 | -------------------------------------------------------------------------------- /router/render-room.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | var React = require('react/addons'); 5 | var ReactApp = React.createFactory(require('../public/components/app')); 6 | 7 | var Gitter = require('../lib/gitter'); 8 | 9 | var room = (req, res) => { 10 | var gitter = new Gitter(req.session.token); 11 | 12 | return Promise.all([ 13 | gitter.get(`/v1/rooms/${req.params.roomId}`, {limit: 50}), 14 | gitter.get(`/v1/rooms/${req.params.roomId}/chatMessages`, {limit: 50}), 15 | gitter.get(`/v1/rooms`) 16 | ]) 17 | .then( values => { 18 | let [room, chatMessages, _rooms] = values; 19 | 20 | var allRooms = _rooms.filter( r => r.lastAccessTime ); 21 | var sortedRooms = _.sortBy(allRooms, 'lastAccessTime').reverse(); 22 | var people = sortedRooms.filter( r => r.oneToOne ).slice(0,10); 23 | var rooms = sortedRooms.filter( r => !r.oneToOne ).slice(0,10); 24 | 25 | var seed = { 26 | room: room, 27 | data: chatMessages, 28 | people: people, 29 | rooms: rooms 30 | }; 31 | 32 | var reactHTML = React.renderToString(new ReactApp({seed: seed})); 33 | res.render('index.ejs', {reactHTML: reactHTML, seed: JSON.stringify(seed)}); 34 | }) 35 | .catch( err => { 36 | console.log('ERR', err); 37 | res.status(500).send('Something went wrong'); 38 | }); 39 | }; 40 | 41 | module.exports = room; 42 | -------------------------------------------------------------------------------- /router/send.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Gitter = require('../lib/gitter'); 4 | 5 | var send = (req, res) => { 6 | if (!req.session.token) return res.redirect('/'); 7 | 8 | var room = req.body.room; 9 | var text = req.body.text; 10 | 11 | var gitter = new Gitter(req.session.token); 12 | gitter.post(`/v1/rooms/${room}/chatMessages`, {}, {text: text}) 13 | .then( (msg) => { 14 | res.status(200).send(msg); 15 | }) 16 | .catch( err => { 17 | console.log('ERR', err); 18 | res.status(500).send('Something went wrong'); 19 | }); 20 | }; 21 | 22 | module.exports = send; 23 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel/register')({extensions: ['.js', '.jsx']}); 4 | 5 | var express = require('express'); 6 | var bodyParser = require('body-parser'); 7 | var serve_static = require('serve-static'); 8 | var compression = require('compression'); 9 | var session = require('cookie-session'); 10 | 11 | var app = express(); 12 | app.use(compression()); 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.urlencoded({extended: true})); 15 | app.set('view engine', 'ejs'); 16 | app.use(serve_static('public')); 17 | app.disable('x-powered-by'); 18 | app.use(session({secret: process.env.SESSION_SECRET})); 19 | 20 | app.get('/', require('./router/home')); 21 | app.use('/oauth', require('./router/oauth')); 22 | app.post('/send', require('./router/send')); 23 | app.get('/rooms/:roomId', require('./router/render-room')); 24 | app.get('/rooms/:roomId/stream', require('./router/msg-stream')); 25 | 26 | var port = process.env.PORT || 4321; 27 | app.listen(port, function() { 28 | console.log('Ready: http://localhost:' + port); 29 | }); 30 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gitter 4 | 5 | 6 | 7 | 8 | 9 | 10 |
<%- reactHTML %>
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /views/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gitter 4 | 5 | 6 | 7 | 8 | 9 | Login
10 | 11 | 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var ET = require("extract-text-webpack-plugin"); 2 | 3 | module.exports = { 4 | entry: { 5 | app: './public/main.jsx' 6 | }, 7 | output: { 8 | filename: './public/build/[name].js' 9 | }, 10 | module: { 11 | loaders: [ 12 | {test: /\.jsx$/, loader: 'jsx-loader?harmony'}, 13 | {test: /\.less$/, loader: ET.extract('style-loader', 'css-loader!less-loader')}, 14 | {test: /\.css$/, loader: ET.extract('style-loader', 'css-loader')}, 15 | ] 16 | }, 17 | resolve: { 18 | extensions: ['', '.json', '.js', '.jsx'] 19 | }, 20 | plugins: [ 21 | new ET("./public/build/[name].css") 22 | ] 23 | }; 24 | --------------------------------------------------------------------------------