├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── README_KOR.md ├── ReadmeVideo.gif ├── config ├── oAuthConfig.dev.js ├── oAuthConfig.prod.js └── passport.js ├── mystreamable.mp4 ├── package-lock.json ├── package.json ├── src ├── client │ └── index.js ├── common │ ├── actions │ │ ├── actions.js │ │ └── authActions.js │ ├── components │ │ ├── ChannelListItem.js │ │ ├── ChannelListModalItem.js │ │ ├── Channels.js │ │ ├── Chat.js │ │ ├── FBSignIn.js │ │ ├── MessageComposer.js │ │ ├── MessageListItem.js │ │ ├── SignIn.js │ │ ├── SignUp.js │ │ ├── TypingListItem.js │ │ ├── UserProfile.js │ │ └── WelcomePage.js │ ├── constants │ │ └── ActionTypes.js │ ├── containers │ │ ├── App.js │ │ ├── ChatContainer.js │ │ └── DevTools.js │ ├── css │ │ └── chatapp.css │ ├── middleware │ │ ├── index.js │ │ └── promiseMiddleware.js │ ├── reducers │ │ ├── activeChannel.js │ │ ├── auth.js │ │ ├── channels.js │ │ ├── environment.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── typers.js │ │ ├── userValidation.js │ │ └── welcomePage.js │ ├── routes.js │ └── store │ │ ├── configureStore.dev.js │ │ ├── configureStore.js │ │ └── configureStore.prod.js └── server │ ├── models │ ├── Channel.js │ ├── Message.js │ └── User.js │ ├── routes │ ├── channel_routes.js │ ├── message_routes.js │ └── user_routes.js │ ├── server.dev.js │ ├── server.js │ ├── server.prod.js │ └── socketEvents.js ├── static └── favicon.ico ├── test ├── TestUtils.js ├── actions.test.js ├── asyncActions.test.js ├── reducers.test.js └── shallowRendering.test.js ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | dist/ 3 | node_modules/ 4 | server/ 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "valid-jsdoc": 2, 10 | 11 | "react/jsx-uses-react": 2, 12 | "react/jsx-uses-vars": 2, 13 | "react/react-in-jsx-scope": 2, 14 | 15 | // Disable until Flow supports let and const 16 | "no-var": 0, 17 | "vars-on-top": 0, 18 | 19 | // Disable comma-dangle unless need to support it 20 | "comma-dangle": 0 21 | }, 22 | "plugins": [ 23 | "react" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/*.sw* 3 | .env 4 | npm-debug.log 5 | db/ 6 | Procfile 7 | .DS_Store 8 | static/ 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-redux-socketio-chat 2 | 3 | ![alt tag](ReadmeVideo.gif) 4 | To see the live version of the app go to http://slackclone.herokuapp.com 5 | 6 | ## Use Guide 7 | 8 | [Korean translation](/README_KOR.md) 9 | 10 | First off, clone the repository and then `cd react-redux-socketio-chat`and `npm install` 11 | 12 | You can create channels with the + sign on the nav bar on the left. 13 | If you click on a user's name to send him a private message (opens a private channel) 14 | 15 | ### Setting up MongoDB 16 | 17 | Note: You need MongoDB set up and running to run the code locally. [Installation instructions](https://docs.mongodb.org/manual/installation/) 18 | 19 | Once you've installed MongoDB start up the MongoDB server in a new terminal with the following commands: 20 | 21 | ``` 22 | mkdir db 23 | mongod --dbpath=./db --smallfiles 24 | ``` 25 | 26 | Then open a new terminal and type in `mongo` and type in `use chat_dev` 27 | This is your database interface. You can query the database for records for example: `db.users.find()` or `db.stats()`. If you want to remove all channels for example you can type `db.channels.remove({})`. 28 | 29 | Now that you've done all that, you can go go ahead and code away! 30 | 31 | ### Development 32 | 33 | ``` 34 | npm run dev 35 | ``` 36 | And then point your browser to `localhost:3000` 37 | 38 | Note: 39 | This program comes with [redux-dev tools](https://github.com/gaearon/redux-devtools) 40 | * To SHOW or HIDE the dev tool panel press ctrl+h 41 | * To change position press ctrl+m 42 | 43 | ### Production 44 | 45 | ``` 46 | npm run build 47 | npm start 48 | ``` 49 | And then point your browser to `localhost:3000` 50 | 51 | ## Helpful Resources and Inspiring Projects 52 | 53 | * Erikras' universal redux example: https://github.com/erikras/react-redux-universal-hot-example 54 | * The facebook react flux-chat example: https://github.com/facebook/flux/tree/master/examples/flux-chat 55 | * The awesome community of reactiflux https://discordapp.com/channels/102860784329052160/102860784329052160 56 | 57 | ## Todos 58 | * Implement virtual scrolling for the chat and channel modal, so that the dom elements load faster! 59 | * Figure out a way to make the initial load quicker, loading only above the fold content? pagination? or some other idea 60 | -------------------------------------------------------------------------------- /README_KOR.md: -------------------------------------------------------------------------------- 1 | # React-redux-socketio-chat - 한국어 번역 2 | 3 | ![alt tag](ReadmeVideo.gif) 4 | 앱의 라이브 버전을 보려면 http://slackclone.herokuapp.com를 방문하십시오. 5 | 6 | ## 사용 안내서 7 | 먼저, 저장소를 복제한 -> ‘cd resact-redux-socketio-chat’ 과 ‘npm install’ 8 | 9 | 왼쪽에 있는 탐색 막대에 + 기호를 눌러 채널을 만들 수 있다. 10 | 사용자 이름을 클릭하여 개인 메시지를 보내는 경우(개인 채널 열기) 11 | 12 | ### MongoDB 설정 13 | 14 | 참고: 코드를 로컬에서 실행하려면 MongoDB를 설정하고 실행해야 한다. [Installation instructions](https://docs.mongodb.org/manual/installation/) 15 | 16 | MongoDB를 설치했으면 다음 명령으로 새 터미널에서 MongoDB 서버를 시작하십시오. 17 | 18 | ``` 19 | mkdir db 20 | mongod --dbpath=./db --smallfiles 21 | ``` 22 | 23 | 그런 다음 새 터미널을 열고 `몽고`를 입력하고 ` chat_dev`를 입력하십시오. 24 | 이것이 당신의 데이터베이스 인터페이스 입니다. 25 | 데이터베이스에 기록을 조회하려면 db.users.find() or db.stats()를 입력하세요 26 | 모든 채널을 제거하려면 db.channels.remove({})를 입력하십시오. 27 | 28 | 그렇게 다 했으니 어서 코드를 뽑아 버려! 29 | 30 | ### Development 31 | ``` 32 | npm run dev 33 | ``` 34 | 그런 다음 브라우저에서 `localhost:3000`을 가리키십시오. 35 | 36 | 참고: 이 프로그램에는 [redux-dev tools](https://github.com/gaearon/redux-devtools)가 제공됨 37 | * Dev 도구 패널을 표시하거나 숨기려면 ctrl+h를 누르십시오. 38 | * 위치를 변경하려면 ctrl+m을 누르십시오. 39 | 40 | ### Production 41 | 42 | ``` 43 | npm run build 44 | npm start 45 | ``` 46 | 그런 다음 브라우저에서 `localhost:3000`을 가리키십시오. 47 | 48 | ## 유용한 리소스 및 프로젝트 활용 49 | 50 | * Erikras의 Resources and 영감을 주는 프로젝트 예: https://github.com/erikras/react-redux-universal-hot-example 51 | * The facebook react flux-chat example: https://github.com/facebook/flux/tree/master/examples/flux-chat 52 | * reactiflux의 멋진 커뮤니티 https://discordapp.com/channels/102860784329052160/102860784329052160 53 | 54 | ## 해야할 일 (Todos) 55 | * 채팅 및 채널 모델에 대한 가상 스크롤을 구현하여 dom 요소가 더 빨리 로드되도록 하십시오! 56 | * 접힌 내용물 위로만 로딩하여 initial load를 더 빠르게 할 수 있는 방법을 알아보십시오. 타원? 또는 다른 생각을 알아보십시오. 57 | -------------------------------------------------------------------------------- /ReadmeVideo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raineroviir/react-redux-socketio-chat/0739e285243a06a8ecef504d885735c9bf268acc/ReadmeVideo.gif -------------------------------------------------------------------------------- /config/oAuthConfig.dev.js: -------------------------------------------------------------------------------- 1 | var ids = { 2 | facebook: { 3 | clientID: '509360805905534', 4 | clientSecret: '6866390927213d6a21946f0fc852a881' 5 | } 6 | } 7 | module.exports = ids; 8 | -------------------------------------------------------------------------------- /config/oAuthConfig.prod.js: -------------------------------------------------------------------------------- 1 | var ids = { 2 | facebook: { 3 | clientID: 'get your own', 4 | clientSecret: 'get your own' 5 | } 6 | } 7 | // please get your own IDs. :) 8 | module.exports = ids; 9 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | var FacebookStrategy = require('passport-facebook').Strategy; 2 | var LocalStrategy = require('passport-local').Strategy; 3 | var User = require('../src/server/models/User'); 4 | var cookies = require('react-cookie'); 5 | 6 | var host = process.env.NODE_ENV !== 'production' ? 'localhost:3000' : 'slackclone.herokuapp.com' 7 | if (process.env.NODE_ENV !== 'production') { 8 | var oAuthConfig = require('./oAuthConfig.dev'); 9 | } else { 10 | var oAuthConfig = require('./oAuthConfig.prod'); 11 | } 12 | 13 | module.exports = function(passport) { 14 | 15 | passport.use('local-signup', new LocalStrategy({ 16 | usernameField: 'username', 17 | passwordField: 'password', 18 | passReqToCallback: true 19 | }, 20 | function(req, username, password, done) { 21 | User.findOne({ 'local.username': username}, function(err, user) { 22 | if (err) { 23 | return done(err); 24 | } 25 | if (user) { 26 | return done(null, false); 27 | } else { 28 | var newUser = new User(); 29 | newUser.local.username = username; 30 | newUser.local.password = newUser.generateHash(password); 31 | newUser.save(function(err, user) { 32 | if (err) { 33 | throw err; 34 | } 35 | return done(null, newUser); 36 | }); 37 | } 38 | }); 39 | })); 40 | 41 | passport.use('local-login', new LocalStrategy({ 42 | usernameField: 'username', 43 | passwordField: 'password', 44 | passReqToCallback: true 45 | }, 46 | function(req, username, password, done) { 47 | User.findOne({ 'local.username': username}, function(err, user) { 48 | if (err) { 49 | return done(err); 50 | } 51 | if (!user) { 52 | return done(null, false); 53 | } 54 | if (!user.validPassword(password)) { 55 | return done(null, false) 56 | } 57 | return done(null, user); 58 | }); 59 | })); 60 | 61 | // NOTE: to set up FB auth you need your own clientID, clientSecret and set up your callbackURL. This can all be done at https://developers.facebook.com/ 62 | passport.use(new FacebookStrategy({ 63 | clientID: oAuthConfig.facebook.clientID, 64 | clientSecret: oAuthConfig.facebook.clientSecret, 65 | callbackURL: "http://" + host + "/api/auth/facebook/callback" 66 | }, 67 | function(accessToken, refreshToken, profile, done) { 68 | cookies.save('username', profile.displayName) 69 | User.findOne({ 'facebook.id': profile.id }, function(err, user) { 70 | if (err) { console.log(err); } 71 | if (!err && user !== null) { 72 | done(null, user); 73 | } else { 74 | var newUser = new User({ 'facebook.id': profile.id, 'facebook.username': profile.displayName}); 75 | newUser.save(function(err, user) { 76 | if (err) { 77 | console.log(err); 78 | } else { 79 | done(null, user); 80 | } 81 | }); 82 | } 83 | }) 84 | } 85 | )); 86 | } 87 | -------------------------------------------------------------------------------- /mystreamable.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raineroviir/react-redux-socketio-chat/0739e285243a06a8ecef504d885735c9bf268acc/mystreamable.mp4 -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React-redux-socketio-chat", 3 | "version": "0.7.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "acorn": { 8 | "version": "5.7.3", 9 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", 10 | "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" 11 | }, 12 | "amdefine": { 13 | "version": "1.0.1", 14 | "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", 15 | "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" 16 | }, 17 | "asap": { 18 | "version": "2.0.6", 19 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 20 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 21 | }, 22 | "ast-types": { 23 | "version": "0.9.6", 24 | "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", 25 | "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=" 26 | }, 27 | "balanced-match": { 28 | "version": "1.0.0", 29 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 30 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 31 | }, 32 | "base62": { 33 | "version": "1.2.8", 34 | "resolved": "https://registry.npmjs.org/base62/-/base62-1.2.8.tgz", 35 | "integrity": "sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA==" 36 | }, 37 | "brace-expansion": { 38 | "version": "1.1.11", 39 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 40 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 41 | "requires": { 42 | "balanced-match": "^1.0.0", 43 | "concat-map": "0.0.1" 44 | } 45 | }, 46 | "commander": { 47 | "version": "2.20.3", 48 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 49 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 50 | }, 51 | "commoner": { 52 | "version": "0.10.8", 53 | "resolved": "https://registry.npmjs.org/commoner/-/commoner-0.10.8.tgz", 54 | "integrity": "sha1-NPw2cs0kOT6LtH5wyqApOBH08sU=", 55 | "requires": { 56 | "commander": "^2.5.0", 57 | "detective": "^4.3.1", 58 | "glob": "^5.0.15", 59 | "graceful-fs": "^4.1.2", 60 | "iconv-lite": "^0.4.5", 61 | "mkdirp": "^0.5.0", 62 | "private": "^0.1.6", 63 | "q": "^1.1.2", 64 | "recast": "^0.11.17" 65 | } 66 | }, 67 | "concat-map": { 68 | "version": "0.0.1", 69 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 70 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 71 | }, 72 | "core-js": { 73 | "version": "1.2.7", 74 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", 75 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" 76 | }, 77 | "defined": { 78 | "version": "1.0.0", 79 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", 80 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" 81 | }, 82 | "detective": { 83 | "version": "4.7.1", 84 | "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", 85 | "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", 86 | "requires": { 87 | "acorn": "^5.2.1", 88 | "defined": "^1.0.0" 89 | } 90 | }, 91 | "envify": { 92 | "version": "3.4.1", 93 | "resolved": "https://registry.npmjs.org/envify/-/envify-3.4.1.tgz", 94 | "integrity": "sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg=", 95 | "requires": { 96 | "jstransform": "^11.0.3", 97 | "through": "~2.3.4" 98 | } 99 | }, 100 | "esprima-fb": { 101 | "version": "15001.1.0-dev-harmony-fb", 102 | "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz", 103 | "integrity": "sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE=" 104 | }, 105 | "fbjs": { 106 | "version": "0.6.1", 107 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.6.1.tgz", 108 | "integrity": "sha1-lja3cF9bqWhNRLcveDISVK/IYPc=", 109 | "requires": { 110 | "core-js": "^1.0.0", 111 | "loose-envify": "^1.0.0", 112 | "promise": "^7.0.3", 113 | "ua-parser-js": "^0.7.9", 114 | "whatwg-fetch": "^0.9.0" 115 | } 116 | }, 117 | "glob": { 118 | "version": "5.0.15", 119 | "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", 120 | "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", 121 | "requires": { 122 | "inflight": "^1.0.4", 123 | "inherits": "2", 124 | "minimatch": "2 || 3", 125 | "once": "^1.3.0", 126 | "path-is-absolute": "^1.0.0" 127 | } 128 | }, 129 | "graceful-fs": { 130 | "version": "4.2.3", 131 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", 132 | "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" 133 | }, 134 | "iconv-lite": { 135 | "version": "0.4.24", 136 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 137 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 138 | "requires": { 139 | "safer-buffer": ">= 2.1.2 < 3" 140 | } 141 | }, 142 | "inflight": { 143 | "version": "1.0.6", 144 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 145 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 146 | "requires": { 147 | "once": "^1.3.0", 148 | "wrappy": "1" 149 | } 150 | }, 151 | "inherits": { 152 | "version": "2.0.4", 153 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 154 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 155 | }, 156 | "js-tokens": { 157 | "version": "4.0.0", 158 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 159 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 160 | }, 161 | "jstransform": { 162 | "version": "11.0.3", 163 | "resolved": "https://registry.npmjs.org/jstransform/-/jstransform-11.0.3.tgz", 164 | "integrity": "sha1-CaeJk+CuTU70SH9hVakfYZDLQiM=", 165 | "requires": { 166 | "base62": "^1.1.0", 167 | "commoner": "^0.10.1", 168 | "esprima-fb": "^15001.1.0-dev-harmony-fb", 169 | "object-assign": "^2.0.0", 170 | "source-map": "^0.4.2" 171 | } 172 | }, 173 | "loose-envify": { 174 | "version": "1.4.0", 175 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 176 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 177 | "requires": { 178 | "js-tokens": "^3.0.0 || ^4.0.0" 179 | } 180 | }, 181 | "minimatch": { 182 | "version": "3.0.4", 183 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 184 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 185 | "requires": { 186 | "brace-expansion": "^1.1.7" 187 | } 188 | }, 189 | "minimist": { 190 | "version": "0.0.8", 191 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 192 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 193 | }, 194 | "mkdirp": { 195 | "version": "0.5.1", 196 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 197 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 198 | "requires": { 199 | "minimist": "0.0.8" 200 | } 201 | }, 202 | "object-assign": { 203 | "version": "2.1.1", 204 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", 205 | "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=" 206 | }, 207 | "once": { 208 | "version": "1.4.0", 209 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 210 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 211 | "requires": { 212 | "wrappy": "1" 213 | } 214 | }, 215 | "path-is-absolute": { 216 | "version": "1.0.1", 217 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 218 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 219 | }, 220 | "private": { 221 | "version": "0.1.8", 222 | "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", 223 | "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" 224 | }, 225 | "promise": { 226 | "version": "7.3.1", 227 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 228 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 229 | "requires": { 230 | "asap": "~2.0.3" 231 | } 232 | }, 233 | "q": { 234 | "version": "1.5.1", 235 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", 236 | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" 237 | }, 238 | "react": { 239 | "version": "0.14.6", 240 | "resolved": "https://registry.npmjs.org/react/-/react-0.14.6.tgz", 241 | "integrity": "sha1-KlfCz4dHtIN1mtjeD6R/sMXPXGo=", 242 | "requires": { 243 | "envify": "^3.0.0", 244 | "fbjs": "^0.6.1" 245 | } 246 | }, 247 | "recast": { 248 | "version": "0.11.23", 249 | "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", 250 | "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", 251 | "requires": { 252 | "ast-types": "0.9.6", 253 | "esprima": "~3.1.0", 254 | "private": "~0.1.5", 255 | "source-map": "~0.5.0" 256 | }, 257 | "dependencies": { 258 | "esprima": { 259 | "version": "3.1.3", 260 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", 261 | "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" 262 | }, 263 | "source-map": { 264 | "version": "0.5.7", 265 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 266 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 267 | } 268 | } 269 | }, 270 | "redux": { 271 | "version": "4.0.4", 272 | "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", 273 | "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", 274 | "requires": { 275 | "loose-envify": "^1.4.0", 276 | "symbol-observable": "^1.2.0" 277 | } 278 | }, 279 | "safer-buffer": { 280 | "version": "2.1.2", 281 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 282 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 283 | }, 284 | "source-map": { 285 | "version": "0.4.4", 286 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", 287 | "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", 288 | "requires": { 289 | "amdefine": ">=0.0.4" 290 | } 291 | }, 292 | "symbol-observable": { 293 | "version": "1.2.0", 294 | "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", 295 | "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" 296 | }, 297 | "through": { 298 | "version": "2.3.8", 299 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 300 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 301 | }, 302 | "ua-parser-js": { 303 | "version": "0.7.20", 304 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", 305 | "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" 306 | }, 307 | "whatwg-fetch": { 308 | "version": "0.9.0", 309 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz", 310 | "integrity": "sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA=" 311 | }, 312 | "wrappy": { 313 | "version": "1.0.2", 314 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 315 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React-redux-socketio-chat", 3 | "version": "0.7.1", 4 | "description": "react frontend, socketio powered chat. redux as the flux implementation", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node src/server/server", 8 | "dev": "node src/server/server", 9 | "test": "mocha './test/**/*.test.js' --compilers js:babel-core/register --recursive", 10 | "test:watch": "npm test -- --watch", 11 | "lint": "eslint 'src'", 12 | "build": "NODE_ENV=production webpack --config ./webpack.config.prod.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/raineroviir/react-redux-socketio-chat" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "reactjs", 21 | "webpack", 22 | "babel", 23 | "redux", 24 | "socket.io", 25 | "redux-devtools", 26 | "redux-promise", 27 | "react-transform" 28 | ], 29 | "author": "roviir@gmail.com", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/raineroviir/react-redux-socketio-chat/issues" 33 | }, 34 | "homepage": "https://github.com/raineroviir/react-redux-socketio-chat", 35 | "devDependencies": { 36 | "babel-eslint": "^4.1.2", 37 | "babel-plugin-react-transform": "^2.0.0", 38 | "css-loader": "^0.15.1", 39 | "eslint": "^1.4.3", 40 | "eslint-config-airbnb": "1.0.0", 41 | "eslint-plugin-react": "^3.4.0", 42 | "expect": "^1.10.0", 43 | "mocha": "^2.3.2", 44 | "mocha-jsdom": "^1.0.0", 45 | "nock": "^2.18.2", 46 | "raw-loader": "^0.5.1", 47 | "react-addons-test-utils": "^0.14.0", 48 | "react-hot-loader": "^1.2.7", 49 | "react-transform-catch-errors": "^1.0.0", 50 | "react-transform-hmr": "^1.0.0", 51 | "redbox-react": "^1.0.6", 52 | "redux-devtools": "^3.0.0", 53 | "redux-devtools-dock-monitor": "^1.0.1", 54 | "redux-devtools-log-monitor": "^1.0.1", 55 | "redux-mock-store": "0.0.4", 56 | "webpack": "^1.9.10", 57 | "webpack-dev-middleware": "^1.2.0", 58 | "webpack-dev-server": "^1.9.0", 59 | "webpack-hot-middleware": "^1.1.0", 60 | "webpack-isomorphic-tools": "^0.8.8" 61 | }, 62 | "dependencies": { 63 | "babel-core": "^6.3.26", 64 | "babel-loader": "^6.2.0", 65 | "babel-polyfill": "^6.3.14", 66 | "babel-preset-es2015": "^6.3.13", 67 | "babel-preset-react": "^6.3.13", 68 | "babel-preset-stage-0": "^6.3.13", 69 | "bcrypt-nodejs": "0.0.3", 70 | "body-parser": "^1.13.0", 71 | "classnames": "^2.1.2", 72 | "clean-webpack-plugin": "^0.1.6", 73 | "cors": "^2.7.1", 74 | "express": "^4.12.4", 75 | "history": "4.6.1", 76 | "isomorphic-fetch": "^2.2.0", 77 | "lodash": "^3.9.3", 78 | "moment": "^2.10.6", 79 | "mongoose": "^4.0.5", 80 | "node-uuid": "^1.4.7", 81 | "passport": "^0.2.2", 82 | "passport-facebook": "^2.0.0", 83 | "passport-local": "^1.0.0", 84 | "react": "^0.14.6", 85 | "react-bootstrap": "^0.28.1", 86 | "react-cookie": "^0.4.3", 87 | "react-dom": "^0.14.6", 88 | "react-redux": "^3.0.1", 89 | "react-router": "^2.0.0-rc4", 90 | "redux": "^4.0.4", 91 | "redux-form": "^4.2.0", 92 | "redux-thunk": "^1.0.0", 93 | "serve-static": "^1.10.0", 94 | "socket.io": "^1.3.5", 95 | "socket.io-client": "^1.4.0", 96 | "style-loader": "^0.12.4" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import '../common/css/chatapp.css'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { browserHistory } from 'react-router'; 5 | import { Router } from 'react-router'; 6 | import { Provider } from 'react-redux'; 7 | import configureStore from '../common/store/configureStore'; 8 | import DevTools from '../common/containers/DevTools'; 9 | import routes from '../common/routes'; 10 | 11 | const initialState = window.__INITIAL_STATE__; 12 | const store = configureStore(initialState); 13 | const rootElement = document.getElementById('react'); 14 | 15 | 16 | ReactDOM.render( 17 | 18 |
19 | 20 | {process.env.NODE_ENV !== 'production' && } 21 |
22 |
, 23 | rootElement 24 | ); 25 | -------------------------------------------------------------------------------- /src/common/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | import { browserHistory } from 'react-router'; 3 | import fetch from 'isomorphic-fetch'; 4 | import moment from 'moment'; 5 | 6 | // NOTE:Chat actions 7 | 8 | function addMessage(message) { 9 | return { 10 | type: types.ADD_MESSAGE, 11 | message 12 | }; 13 | } 14 | 15 | export function receiveRawMessage(message) { 16 | return { 17 | type: types.RECEIVE_MESSAGE, 18 | message 19 | }; 20 | } 21 | 22 | export function receiveRawChannel(channel) { 23 | return { 24 | type: types.RECEIVE_CHANNEL, 25 | channel 26 | }; 27 | } 28 | 29 | function addChannel(channel) { 30 | return { 31 | type: types.ADD_CHANNEL, 32 | channel 33 | }; 34 | } 35 | 36 | export function typing(username) { 37 | return { 38 | type: types.TYPING, 39 | username 40 | }; 41 | } 42 | 43 | export function stopTyping(username) { 44 | return { 45 | type: types.STOP_TYPING, 46 | username 47 | }; 48 | } 49 | 50 | export function changeChannel(channel) { 51 | return { 52 | type: types.CHANGE_CHANNEL, 53 | channel 54 | }; 55 | } 56 | 57 | // NOTE:Data Fetching actions 58 | 59 | export function welcomePage(username) { 60 | return { 61 | type: types.SAVE_USERNAME, 62 | username 63 | }; 64 | } 65 | 66 | export function fetchChannels(user) { 67 | return dispatch => { 68 | dispatch(requestChannels()) 69 | return fetch(`/api/channels/${user}`) 70 | .then(response => response.json()) 71 | .then(json => dispatch(receiveChannels(json))) 72 | .catch(error => {throw error}); 73 | } 74 | } 75 | 76 | function requestChannels() { 77 | return { 78 | type: types.LOAD_CHANNELS 79 | } 80 | } 81 | 82 | function receiveChannels(json) { 83 | return { 84 | type: types.LOAD_CHANNELS_SUCCESS, 85 | json 86 | } 87 | } 88 | 89 | function requestMessages() { 90 | return { 91 | type: types.LOAD_MESSAGES 92 | } 93 | } 94 | 95 | export function fetchMessages(channel) { 96 | return dispatch => { 97 | dispatch(requestMessages()) 98 | return fetch(`/api/messages/${channel}`) 99 | .then(response => response.json()) 100 | .then(json => dispatch(receiveMessages(json, channel))) 101 | .catch(error => {throw error}); 102 | } 103 | } 104 | 105 | function receiveMessages(json, channel) { 106 | const date = moment().format('lll'); 107 | return { 108 | type: types.LOAD_MESSAGES_SUCCESS, 109 | json, 110 | channel, 111 | date 112 | } 113 | } 114 | 115 | function loadingValidationList() { 116 | return { 117 | type: types.LOAD_USERVALIDATION 118 | } 119 | } 120 | 121 | function receiveValidationList(json) { 122 | return { 123 | type: types.LOAD_USERVALIDATION_SUCCESS, 124 | json 125 | } 126 | } 127 | 128 | export function usernameValidationList() { 129 | return dispatch => { 130 | dispatch(loadingValidationList()) 131 | return fetch('/api/all_usernames') 132 | .then(response => { 133 | return response.json() 134 | }) 135 | .then(json => { 136 | return dispatch(receiveValidationList(json.map((item) => item.local.username))) 137 | }) 138 | .catch(error => {throw error}); 139 | } 140 | } 141 | 142 | export function createMessage(message) { 143 | return dispatch => { 144 | dispatch(addMessage(message)) 145 | return fetch('/api/newmessage', { 146 | method: 'post', 147 | headers: { 148 | 'Content-Type': 'application/json' 149 | }, 150 | body: JSON.stringify(message)}) 151 | .catch(error => {throw error}); 152 | } 153 | } 154 | 155 | export function createChannel(channel) { 156 | return dispatch => { 157 | return fetch('/api/channels/new_channel', { 158 | method: 'post', 159 | headers: { 160 | 'Content-Type': 'application/json' 161 | }, 162 | body: JSON.stringify(channel)}) 163 | .catch(error => {throw error}).then((val) => 164 | {}, 165 | () => {dispatch(addChannel(channel))}) 166 | } 167 | } 168 | 169 | //the environment code is borrowed from Andrew Ngu, https://github.com/andrewngu/sound-redux 170 | 171 | function changeIsMobile(isMobile) { 172 | return { 173 | type: types.CHANGE_IS_MOBILE, 174 | isMobile 175 | }; 176 | } 177 | 178 | function changeWidthAndHeight(screenHeight, screenWidth) { 179 | return { 180 | type: types.CHANGE_WIDTH_AND_HEIGHT, 181 | screenHeight, 182 | screenWidth 183 | }; 184 | } 185 | 186 | export function initEnvironment() { 187 | return dispatch => { 188 | const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 189 | if (isMobile) { 190 | document.body.style.overflow = 'hidden'; 191 | } 192 | 193 | dispatch(changeIsMobile(isMobile)); 194 | dispatch(changeWidthAndHeight(window.innerHeight, window.innerWidth)); 195 | 196 | window.onresize = () => { 197 | dispatch(changeWidthAndHeight(window.innerHeight, window.innerWidth)); 198 | } 199 | }; 200 | } 201 | -------------------------------------------------------------------------------- /src/common/actions/authActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | import { browserHistory } from 'react-router'; 3 | import fetch from 'isomorphic-fetch'; 4 | import cookie from 'react-cookie'; 5 | 6 | export function receiveAuth() { 7 | const user = cookie.load('username'); 8 | return { 9 | type: types.AUTH_LOAD_SUCCESS, 10 | user 11 | } 12 | } 13 | 14 | export function checkAuth() { 15 | if (cookie.load('username')) { 16 | return true; 17 | } 18 | return false; 19 | } 20 | 21 | function requestSignUp() { 22 | return { 23 | type: types.AUTH_SIGNUP 24 | } 25 | } 26 | 27 | function receiveUser(username) { 28 | const newUser = { 29 | name: username, 30 | id: Symbol(username) 31 | } 32 | return { 33 | type: types.AUTH_SIGNUP_SUCCESS, 34 | newUser 35 | } 36 | } 37 | 38 | function requestSignOut() { 39 | return { 40 | type: types.AUTH_SIGNOUT 41 | } 42 | } 43 | function receiveSignOut() { 44 | return { 45 | type: types.AUTH_SIGNOUT_SUCCESS 46 | } 47 | } 48 | 49 | export function signOut() { 50 | return dispatch => { 51 | dispatch(requestSignOut()) 52 | return fetch('/api/signout') 53 | .then(response => { 54 | if(response.ok) { 55 | cookie.remove('username') 56 | dispatch(receiveSignOut()) 57 | browserHistory.push('/') 58 | } 59 | }) 60 | .catch(error => {throw error}); 61 | } 62 | } 63 | 64 | export function signUp(user) { 65 | return dispatch => { 66 | dispatch(requestSignUp()) 67 | return fetch('/api/sign_up', { 68 | method: 'post', 69 | headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' 70 | }, 71 | body: JSON.stringify(user) 72 | }) 73 | .then(response => { 74 | if(response.ok) { 75 | cookie.save('username', user.username) 76 | dispatch(receiveUser(user.username)); 77 | browserHistory.push('/chat'); 78 | } 79 | }) 80 | .catch(error => {throw error}); 81 | }; 82 | } 83 | 84 | function requestSignIn() { 85 | return { 86 | type: types.AUTH_SIGNIN 87 | } 88 | } 89 | 90 | function receiveSignIn(username) { 91 | const user = { 92 | name: username, 93 | id: Symbol(username) 94 | } 95 | return { 96 | type: types.AUTH_SIGNIN_SUCCESS, 97 | user 98 | } 99 | } 100 | 101 | export function signIn(user) { 102 | return dispatch => { 103 | dispatch(requestSignIn()) 104 | return fetch('/api/sign_in', { 105 | method: 'post', 106 | headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' 107 | }, 108 | body: JSON.stringify(user) 109 | }) 110 | .then(response => { 111 | if(response.ok) { 112 | cookie.save('username', user.username) 113 | dispatch(receiveSignIn(user.username)); 114 | browserHistory.push('/chat'); 115 | } 116 | }) 117 | .catch(error => {throw error}); 118 | }; 119 | } 120 | 121 | export function receiveSocket(socketID) { 122 | return { 123 | type: types.RECEIVE_SOCKET, 124 | socketID 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/common/components/ChannelListItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classnames from 'classnames'; 3 | import { Button } from 'react-bootstrap'; 4 | 5 | const ChannelListItem = (props) => { 6 | const { channel: selectedChannel, onClick, channel } = props; 7 | return ( 8 | 17 | ); 18 | } 19 | 20 | ChannelListItem.propTypes = { 21 | channel: PropTypes.object.isRequired, 22 | onClick: PropTypes.func.isRequired 23 | } 24 | 25 | export default ChannelListItem; 26 | -------------------------------------------------------------------------------- /src/common/components/ChannelListModalItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | const ChannelListModalItem = (props) => { 5 | const { channel: selectedChannel, onClick, channel } = props; 6 | return ( 7 | onClick(channel)}> 10 |
  • 11 |
    {channel.name}
    12 |
  • 13 |
    14 | ); 15 | } 16 | 17 | ChannelListModalItem.propTypes = { 18 | channel: PropTypes.object.isRequired, 19 | onClick: PropTypes.func.isRequired 20 | } 21 | 22 | export default ChannelListModalItem; 23 | -------------------------------------------------------------------------------- /src/common/components/Channels.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ChannelListItem from './ChannelListItem'; 3 | import ChannelListModalItem from './ChannelListModalItem'; 4 | import { Modal, Glyphicon, Input, Button } from 'react-bootstrap'; 5 | import * as actions from '../actions/actions'; 6 | import uuid from 'node-uuid'; 7 | 8 | export default class Channels extends Component { 9 | 10 | static propTypes = { 11 | channels: PropTypes.array.isRequired, 12 | onClick: PropTypes.func.isRequired, 13 | messages: PropTypes.array.isRequired, 14 | dispatch: PropTypes.func.isRequired 15 | }; 16 | constructor(props, context) { 17 | super(props, context); 18 | this.state = { 19 | addChannelModal: false, 20 | channelName: '', 21 | moreChannelsModal: false 22 | }; 23 | } 24 | handleChangeChannel(channel) { 25 | if(this.state.moreChannelsModal) { 26 | this.closeMoreChannelsModal(); 27 | } 28 | this.props.onClick(channel); 29 | } 30 | openAddChannelModal(event) { 31 | event.preventDefault(); 32 | this.setState({addChannelModal: true}); 33 | } 34 | closeAddChannelModal(event) { 35 | event.preventDefault(); 36 | this.setState({addChannelModal: false}); 37 | } 38 | handleModalChange(event) { 39 | this.setState({channelName: event.target.value}); 40 | } 41 | handleModalSubmit(event) { 42 | const { channels, dispatch, socket } = this.props; 43 | event.preventDefault(); 44 | if (this.state.channelName.length < 1) { 45 | this.refs.channelName.getInputDOMNode().focus(); 46 | } 47 | if (this.state.channelName.length > 0 && channels.filter(channel => { 48 | return channel.name === this.state.channelName.trim(); 49 | }).length < 1) { 50 | const newChannel = { 51 | name: this.state.channelName.trim(), 52 | id: `${Date.now()}${uuid.v4()}`, 53 | private: false 54 | }; 55 | dispatch(actions.createChannel(newChannel)); 56 | this.handleChangeChannel(newChannel); 57 | socket.emit('new channel', newChannel); 58 | this.setState({channelName: ''}); 59 | this.closeAddChannelModal(); 60 | } 61 | } 62 | validateChannelName() { 63 | const { channels } = this.props; 64 | if (channels.filter(channel => { 65 | return channel.name === this.state.channelName.trim(); 66 | }).length > 0) { 67 | return 'error'; 68 | } 69 | return 'success'; 70 | } 71 | openMoreChannelsModal(event) { 72 | event.preventDefault(); 73 | this.setState({moreChannelsModal: true}); 74 | } 75 | closeMoreChannelsModal(event) { 76 | event.preventDefault(); 77 | this.setState({moreChannelsModal: false}); 78 | } 79 | createChannelWithinModal() { 80 | this.closeMoreChannelsModal(); 81 | this.openAddChannelModal(); 82 | } 83 | render() { 84 | const { channels, messages } = this.props; 85 | const filteredChannels = channels.slice(0, 8); 86 | const moreChannelsBoolean = channels.length > 8; 87 | const restOfTheChannels = channels.slice(8); 88 | const newChannelModal = ( 89 |
    90 | 91 | 92 | Add New Channel 93 | 94 | 95 |
    96 | 108 |
    109 |
    110 | 111 | 112 | 115 | 116 |
    117 |
    118 | ); 119 | const moreChannelsModal = ( 120 |
    121 | 122 | 123 | More Channels 124 | 125 | Create a channel 126 | 127 | 128 | 129 |
      130 | {restOfTheChannels.map(channel => 131 | 132 | )} 133 |
    134 |
    135 | 136 | 137 | 138 |
    139 |
    140 | ); 141 | return ( 142 |
    143 |
    144 | 145 | Channels 146 | 149 | 150 |
    151 | {newChannelModal} 152 |
    153 | 160 | {moreChannelsBoolean && + {channels.length - 8} more...} 161 | {moreChannelsModal} 162 |
    163 |
    164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/common/components/Chat.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import MessageComposer from './MessageComposer'; 3 | import MessageListItem from './MessageListItem'; 4 | import Channels from './Channels'; 5 | import * as actions from '../actions/actions'; 6 | import * as authActions from '../actions/authActions'; 7 | import TypingListItem from './TypingListItem'; 8 | import { Modal, DropdownButton, MenuItem, Button, Navbar, NavDropdown, Nav, NavItem } from 'react-bootstrap'; 9 | 10 | export default class Chat extends Component { 11 | 12 | static propTypes = { 13 | messages: PropTypes.array.isRequired, 14 | user: PropTypes.object.isRequired, 15 | dispatch: PropTypes.func.isRequired, 16 | channels: PropTypes.array.isRequired, 17 | activeChannel: PropTypes.string.isRequired, 18 | typers: PropTypes.array.isRequired, 19 | socket: PropTypes.object.isRequired 20 | }; 21 | constructor(props, context) { 22 | super(props, context); 23 | this.state = { 24 | privateChannelModal: false, 25 | targetedUser: '' 26 | } 27 | } 28 | componentDidMount() { 29 | const { socket, user, dispatch } = this.props; 30 | socket.emit('chat mounted', user); 31 | socket.on('new bc message', msg => 32 | dispatch(actions.receiveRawMessage(msg)) 33 | ); 34 | socket.on('typing bc', user => 35 | dispatch(actions.typing(user)) 36 | ); 37 | socket.on('stop typing bc', user => 38 | dispatch(actions.stopTyping(user)) 39 | ); 40 | socket.on('new channel', channel => 41 | dispatch(actions.receiveRawChannel(channel)) 42 | ); 43 | socket.on('receive socket', socketID => 44 | dispatch(authActions.receiveSocket(socketID)) 45 | ); 46 | socket.on('receive private channel', channel => 47 | dispatch(actions.receiveRawChannel(channel)) 48 | ); 49 | } 50 | componentDidUpdate() { 51 | const messageList = this.refs.messageList; 52 | messageList.scrollTop = messageList.scrollHeight; 53 | } 54 | handleSave(newMessage) { 55 | const { dispatch } = this.props; 56 | if (newMessage.text.length !== 0) { 57 | dispatch(actions.createMessage(newMessage)); 58 | } 59 | } 60 | handleSignOut() { 61 | const { dispatch } = this.props; 62 | dispatch(authActions.signOut()); 63 | } 64 | changeActiveChannel(channel) { 65 | const { socket, activeChannel, dispatch } = this.props; 66 | socket.emit('leave channel', activeChannel); 67 | socket.emit('join channel', channel); 68 | dispatch(actions.changeChannel(channel)); 69 | dispatch(actions.fetchMessages(channel.name)); 70 | } 71 | handleClickOnUser(user) { 72 | this.setState({ privateChannelModal: true, targetedUser: user }); 73 | } 74 | closePrivateChannelModal(event) { 75 | event.preventDefault(); 76 | this.setState({privateChannelModal: false}); 77 | } 78 | handleSendDirectMessage() { 79 | const { dispatch, socket, channels, user } = this.props; 80 | const doesPrivateChannelExist = channels.filter(item => { 81 | return item.name === (`${this.state.targetedUser.username}+${user.username}` || `${user.username}+${this.state.targetedUser.username}`) 82 | }) 83 | if (user.username !== this.state.targetedUser.username && doesPrivateChannelExist.length === 0) { 84 | const newChannel = { 85 | name: `${this.state.targetedUser.username}+${user.username}`, 86 | id: Date.now(), 87 | private: true, 88 | between: [this.state.targetedUser.username, user.username] 89 | }; 90 | dispatch(actions.createChannel(newChannel)); 91 | this.changeActiveChannel(newChannel); 92 | socket.emit('new private channel', this.state.targetedUser.socketID, newChannel); 93 | } 94 | if(doesPrivateChannelExist.length > 0) { 95 | this.changeActiveChannel(doesPrivateChannelExist[0]); 96 | } 97 | this.setState({ privateChannelModal: false, targetedUser: '' }); 98 | } 99 | render() { 100 | const { messages, socket, channels, activeChannel, typers, dispatch, user, screenWidth} = this.props; 101 | const filteredMessages = messages.filter(message => message.channelID === activeChannel); 102 | const username = this.props.user.username; 103 | const dropDownMenu = ( 104 |
    105 | 106 | Sign out 107 | 108 |
    109 | ); 110 | const PrivateMessageModal = ( 111 |
    112 | 113 | 114 | {this.state.targetedUser.username} 115 | 116 | 117 | 120 | 121 | 122 | 125 | 126 | 127 |
    128 | ); 129 | const mobileNav = ( 130 | 131 | {username} 132 | 133 | 134 | 136 |
    137 | 138 |
    139 |
    140 |
    141 | ); 142 | const bigNav = ( 143 |
    144 | {dropDownMenu} 145 |
    146 | 147 |
    148 |
    149 | ); 150 | return ( 151 |
    152 | {screenWidth < 500 ? mobileNav : bigNav } 153 |
    154 |
    155 |
    156 | {activeChannel} 157 |
    158 |
    159 | {PrivateMessageModal} 160 | 165 | 166 |
    167 | 189 |
    190 | ); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/common/components/FBSignIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | 4 | const FBSignIn = (props) => { 5 | return ( 6 |
    7 | 8 | 16 | 17 |
    18 | ); 19 | } 20 | 21 | export default FBSignIn; 22 | -------------------------------------------------------------------------------- /src/common/components/MessageComposer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import moment from 'moment'; 3 | import { Input } from 'react-bootstrap'; 4 | import uuid from 'node-uuid'; 5 | 6 | export default class MessageComposer extends Component { 7 | 8 | static propTypes = { 9 | activeChannel: PropTypes.string.isRequired, 10 | onSave: PropTypes.func.isRequired, 11 | user: PropTypes.object.isRequired, 12 | socket: PropTypes.object.isRequired 13 | }; 14 | constructor(props, context) { 15 | super(props, context); 16 | this.state = { 17 | text: '', 18 | typing: false 19 | }; 20 | } 21 | handleSubmit(event) { 22 | const { user, socket, activeChannel} = this.props; 23 | const text = event.target.value.trim(); 24 | if (event.which === 13) { 25 | event.preventDefault(); 26 | var newMessage = { 27 | id: `${Date.now()}${uuid.v4()}`, 28 | channelID: this.props.activeChannel, 29 | text: text, 30 | user: user, 31 | time: moment.utc().format('lll') 32 | }; 33 | socket.emit('new message', newMessage); 34 | socket.emit('stop typing', { user: user.username, channel: activeChannel }); 35 | this.props.onSave(newMessage); 36 | this.setState({ text: '', typing: false }); 37 | } 38 | } 39 | handleChange(event) { 40 | const { socket, user, activeChannel } = this.props; 41 | this.setState({ text: event.target.value }); 42 | if (event.target.value.length > 0 && !this.state.typing) { 43 | socket.emit('typing', { user: user.username, channel: activeChannel }); 44 | this.setState({ typing: true}); 45 | } 46 | if (event.target.value.length === 0 && this.state.typing) { 47 | socket.emit('stop typing', { user: user.username, channel: activeChannel }); 48 | this.setState({ typing: false}); 49 | } 50 | } 51 | render() { 52 | return ( 53 |
    62 | 77 |
    78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/common/components/MessageListItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | export default class MessageListItem extends React.Component { 4 | static propTypes = { 5 | message: PropTypes.object.isRequired 6 | }; 7 | handleClick(user) { 8 | this.props.handleClickOnUser(user); 9 | } 10 | render() { 11 | const { message } = this.props; 12 | return ( 13 |
  • 14 | 15 | 16 | {message.time} 17 | 18 |
    {message.text}
    19 |
  • 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/components/SignIn.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Button, Input } from 'react-bootstrap'; 4 | import * as authActions from '../actions/authActions'; 5 | 6 | class SignIn extends Component { 7 | 8 | static propTypes = { 9 | welcomePage: PropTypes.string.isRequired, 10 | dispatch: PropTypes.func.isRequired 11 | }; 12 | constructor(props, context) { 13 | super(props, context); 14 | this.state = { 15 | username: this.props.welcomePage || '', 16 | password: '' 17 | }; 18 | } 19 | componentDidMount() { 20 | if (this.state.username.length) { 21 | this.refs.passwordInput.getInputDOMNode().focus(); 22 | } else { 23 | this.refs.usernameInput.getInputDOMNode().focus(); 24 | } 25 | } 26 | handleChange(event) { 27 | if (event.target.name === 'username') { 28 | this.setState({ username: event.target.value }); 29 | } 30 | if (event.target.name === 'password') { 31 | this.setState({ password: event.target.value }); 32 | } 33 | } 34 | handleSubmit(event) { 35 | event.preventDefault(); 36 | const { dispatch } = this.props; 37 | if (this.state.username.length < 1) { 38 | this.refs.usernameInput.getInputDOMNode().focus(); 39 | } 40 | if (this.state.username.length > 0 && this.state.password.length < 1) { 41 | this.refs.passwordInput.getInputDOMNode().focus(); 42 | } 43 | if (this.state.username.length > 0 && this.state.password.length > 0) { 44 | var userObj = { 45 | username: this.state.username, 46 | password: this.state.password 47 | }; 48 | dispatch(authActions.signIn(userObj)) 49 | this.setState({ username: '', password: ''}); 50 | } 51 | } 52 | render() { 53 | return ( 54 |
    55 |
    56 | Sign In to Chat 57 |
    58 |
    59 |
    60 | 69 | 78 | 84 |
    85 |
    86 |
    87 | ); 88 | } 89 | } 90 | 91 | function mapStateToProps(state) { 92 | return { 93 | welcomePage: state.welcomePage, 94 | } 95 | } 96 | export default connect(mapStateToProps)(SignIn) 97 | -------------------------------------------------------------------------------- /src/common/components/SignUp.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/actions'; 4 | import { Input, Button } from 'react-bootstrap'; 5 | import * as authActions from '../actions/authActions'; 6 | 7 | class SignUp extends Component { 8 | 9 | static propTypes = { 10 | welcomePage: PropTypes.string.isRequired, 11 | userValidation: PropTypes.array.isrequired, 12 | dispatch: PropTypes.func.isRequired 13 | }; 14 | constructor(props, context) { 15 | super(props, context); 16 | this.state = { 17 | username: this.props.welcomePage || '', 18 | password: '', 19 | confirmPassword: '' 20 | }; 21 | } 22 | componentWillMount() { 23 | const { dispatch, userValidation } = this.props; 24 | if(userValidation.length === 0) { 25 | dispatch(actions.usernameValidationList()); 26 | } 27 | } 28 | componentDidMount() { 29 | if (this.state.username.length) { 30 | this.refs.passwordInput.getInputDOMNode().focus(); 31 | } else { 32 | this.refs.usernameInput.getInputDOMNode().focus(); 33 | } 34 | } 35 | handleSubmit(event) { 36 | event.preventDefault(); 37 | const { dispatch } = this.props; 38 | if (!this.state.username.length) { 39 | this.refs.usernameInput.getInputDOMNode().focus(); 40 | } 41 | if (this.state.username.length && !this.state.password.length) { 42 | this.refs.passwordInput.getInputDOMNode().focus(); 43 | } 44 | if (this.state.username.length && this.state.password.length && !this.state.confirmPassword.length) { 45 | this.refs.confirmPasswordInput.getInputDOMNode().focus(); 46 | } 47 | if (this.state.username.length && this.state.password.length && this.state.confirmPassword.length) { 48 | const userObj = { 49 | username: this.state.username, 50 | password: this.state.password, 51 | confirmPassword: this.state.confirmPassword 52 | }; 53 | dispatch(authActions.signUp(userObj)) 54 | const initLobby = { 55 | name: "Lobby", 56 | id: 0, 57 | private: false 58 | }; 59 | dispatch(actions.createChannel(initLobby)); 60 | this.setState({ username: '', password: '', confirmPassword: ''}); 61 | } 62 | } 63 | handleChange(event) { 64 | if (event.target.name === 'username') { 65 | this.setState({ username: event.target.value }); 66 | } 67 | if (event.target.name === 'password') { 68 | this.setState({ password: event.target.value }); 69 | } 70 | if (event.target.name === 'confirm-password') { 71 | this.setState({ confirmPassword: event.target.value }); 72 | } 73 | } 74 | validateUsername() { 75 | const { userValidation } = this.props; 76 | if (userValidation.filter(user => { 77 | return user === this.state.username.trim(); 78 | }).length > 0) { 79 | return 'error'; 80 | } 81 | return 'success'; 82 | } 83 | validateConfirmPassword() { 84 | if (this.state.confirmPassword.length > 0 && this.state.password.length > 0) { 85 | if (this.state.password === this.state.confirmPassword) { 86 | return 'success'; 87 | } 88 | return 'error'; 89 | } 90 | } 91 | render() { 92 | return ( 93 |
    94 |
    95 | Sign Up 96 |
    97 |
    98 |
    99 |
    100 | 113 |
    114 |
    115 | 124 |
    125 |
    126 | 135 |
    136 | 144 |
    145 |
    146 |
    147 | ); 148 | } 149 | } 150 | 151 | function mapStateToProps(state) { 152 | return { 153 | welcomePage: state.welcomePage, 154 | userValidation: state.userValidation.data 155 | } 156 | } 157 | 158 | export default connect(mapStateToProps)(SignUp) 159 | -------------------------------------------------------------------------------- /src/common/components/TypingListItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | const TypingListItem = (props) => { 4 | const { username } = props; 5 | return ( 6 | 7 | {username} 8 | 9 | ); 10 | } 11 | 12 | TypingListItem.proptypes = { 13 | username: PropTypes.string.isRequired 14 | } 15 | 16 | export default TypingListItem; 17 | -------------------------------------------------------------------------------- /src/common/components/UserProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export class UserProfile extends React.Component { 4 | render() { 5 | return ( 6 |
    7 | User's Profile 8 |
    9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/common/components/WelcomePage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | import {welcomePage} from '../actions/actions'; 4 | import { connect } from 'react-redux'; 5 | import { Input, Button } from 'react-bootstrap'; 6 | import FBSignIn from './FBSignIn'; 7 | import SignIn from './SignIn'; 8 | 9 | class WelcomePage extends Component { 10 | 11 | static propTypes = { 12 | dispatch: PropTypes.func.isRequired 13 | }; 14 | constructor(props, context) { 15 | super(props, context); 16 | this.state = { 17 | username: '' 18 | }; 19 | } 20 | componentDidMount() { 21 | this.refs.usernameInput.getInputDOMNode().focus(); 22 | } 23 | handleChange(event) { 24 | if (event.target.name === 'username') { 25 | this.setState({ username: event.target.value }); 26 | } 27 | } 28 | handleSubmit() { 29 | const { dispatch } = this.props; 30 | const username = this.state.username; 31 | dispatch(welcomePage(username)); 32 | this.setState({ username: '' }); 33 | } 34 | render() { 35 | const {screenWidth} = this.props; 36 | if(screenWidth < 500) { 37 | return ( 38 |
    39 |
    40 |

    Welcome to React Redux Socket.io Chat

    41 |

    This is an open source chat program.

    42 |
    43 |
    44 |
    45 | 54 | 55 | 62 | 63 |
    64 |

    Or

    65 | 66 | 67 | 68 |
    69 |
    70 | ); 71 | } 72 | return ( 73 |
    74 |
    75 |

    Welcome to React Redux Socket.io Chat

    76 |

    77 | This is an open source chat program. 78 |

    79 |
    80 |
    81 |
    82 | 83 |
    84 |
    85 | 94 |
    95 |
    96 | 97 | 104 | 105 |
    106 |
    107 |
    108 |

    Or

    109 | 110 | 111 | 112 |
    113 |
    114 |
    115 | ); 116 | } 117 | } 118 | 119 | function mapStateToProps(state) { 120 | return { 121 | screenWidth: state.environment.screenWidth 122 | } 123 | } 124 | 125 | export default connect(mapStateToProps)(WelcomePage) 126 | -------------------------------------------------------------------------------- /src/common/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_MESSAGE = 'ADD_MESSAGE'; 2 | export const RECEIVE_MESSAGE = 'RECEIVE_MESSAGE'; 3 | 4 | export const ADD_CHANNEL = 'ADD_CHANNEL'; 5 | export const CHANGE_CHANNEL = 'CHANGE_CHANNEL'; 6 | export const RECEIVE_CHANNEL = 'RECEIVE_CHANNEL'; 7 | 8 | export const SIGNUP_USER = 'SIGNUP_USER'; 9 | export const SIGNIN = 'SIGNIN'; 10 | 11 | export const AUTH_LOAD = 'AUTH_LOAD'; 12 | export const AUTH_LOAD_SUCCESS = 'AUTH_LOAD_SUCCESS'; 13 | export const AUTH_LOAD_FAIL = 'AUTH_LOAD_FAIL'; 14 | 15 | export const AUTH_SIGNIN = 'AUTH_SIGNIN'; 16 | export const AUTH_SIGNIN_SUCCESS = 'AUTH_SIGNIN_SUCCESS'; 17 | export const AUTH_SIGNIN_FAIL = 'AUTH_SIGNIN_FAIL'; 18 | 19 | export const AUTH_SIGNOUT = 'AUTH_SIGNOUT'; 20 | export const AUTH_SIGNOUT_SUCCESS = 'AUTH_SIGNOUT_SUCCESS'; 21 | export const AUTH_SIGNOUT_FAIL = 'AUTH_SIGNOUT_FAIL'; 22 | 23 | export const AUTH_SIGNUP_SUCCESS = 'AUTH_SIGNUP_SUCCESS'; 24 | export const AUTH_SIGNUP = 'AUTH_SIGNUP'; 25 | export const AUTH_SIGNUP_FAIL = 'AUTH_SIGNUP_FAIL'; 26 | 27 | export const TYPING = 'TYPING'; 28 | export const STOP_TYPING = 'STOP_TYPING'; 29 | 30 | export const ADD_USER = 'ADD_USER'; 31 | 32 | export const REMOVE_USER_FROM_CHANNEL = 'REMOVE_USER_FROM_CHANNEL'; 33 | 34 | export const SAVE_USERNAME = 'SAVE_USERNAME'; 35 | 36 | export const LOAD_MESSAGES = 'LOAD_MESSAGES'; 37 | export const LOAD_MESSAGES_SUCCESS = 'LOAD_MESSAGES_SUCCESS'; 38 | export const LOAD_MESSAGES_FAIL = 'LOAD_MESSAGES_FAIL'; 39 | 40 | export const LOAD_CHANNELS = 'LOAD_CHANNELS'; 41 | export const LOAD_CHANNELS_SUCCESS = 'LOAD_CHANNELS_SUCCESS'; 42 | export const LOAD_CHANNELS_FAIL = 'LOAD_CHANNELS_FAIL'; 43 | 44 | export const LOAD_USERSONLINE = 'LOAD_USERSONLINE'; 45 | export const LOAD_USERSONLINE_SUCCESS = 'LOAD_USERSONLINE_SUCCESS'; 46 | export const LOAD_USERSONLINE_FAIL = 'LOAD_USERSONLINE_FAIL'; 47 | 48 | export const RECEIVE_USER = 'RECEIVE_USER'; 49 | 50 | export const START_USERNAME_VALIDATION = 'START_USERNAME_VALIDATION'; 51 | export const USERNAME_VALIDATION_SUCCESS = 'USERNAME_VALIDATION_SUCCESS'; 52 | export const USERNAME_VALIDATION_FAIL = 'USERNAME_VALIDATION_FAIL'; 53 | 54 | export const LOAD_USERVALIDATION = 'LOAD_USERVALIDATION'; 55 | export const LOAD_USERVALIDATION_SUCCESS = 'LOAD_USERVALIDATION_SUCCESS'; 56 | export const LOAD_USERVALIDATION_FAIL = 'LOAD_USERVALIDATION_FAIL'; 57 | 58 | export const RECEIVE_MESSAGES = 'RECEIVE_MESSAGES'; 59 | 60 | export const CHANGE_IS_MOBILE = 'CHANGE_IS_MOBILE'; 61 | export const CHANGE_WIDTH_AND_HEIGHT = 'CHANGE_WIDTH_AND_HEIGHT'; 62 | 63 | export const RECEIVE_SOCKET = 'RECEIVE_SOCKET'; 64 | 65 | -------------------------------------------------------------------------------- /src/common/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | import {initEnvironment} from '../actions/actions'; 4 | import { connect } from 'react-redux'; 5 | import * as actions from '../actions/actions'; 6 | 7 | class App extends React.Component { 8 | 9 | componentDidMount() { 10 | const {dispatch} = this.props; 11 | dispatch(initEnvironment()); 12 | } 13 | render() { 14 | const {screenHeight, isMobile, screenWidth} = this.props.environment; 15 | if (isMobile) { 16 | return ( 17 |
    18 | {this.props.children} 19 |
    20 | ); 21 | } 22 | return ( 23 |
    24 | {this.props.children} 25 |
    26 | ); 27 | } 28 | } 29 | 30 | function mapStateToProps(state) { 31 | return { 32 | environment: state.environment 33 | } 34 | } 35 | 36 | export default connect(mapStateToProps)(App) 37 | -------------------------------------------------------------------------------- /src/common/containers/ChatContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import * as actions from '../actions/actions'; 3 | import {receiveAuth} from '../actions/authActions'; 4 | import Chat from '../components/Chat'; 5 | import { bindActionCreators } from 'redux'; 6 | import { connect } from 'react-redux'; 7 | import io from 'socket.io-client'; 8 | 9 | const socket = io('', { path: '/api/chat' }); 10 | const initialChannel = 'Lobby'; // NOTE: I hard coded this value for my example. Change this as you see fit 11 | 12 | class ChatContainer extends Component { 13 | componentDidMount() { 14 | const { dispatch, user } = this.props; 15 | if(!user.username) { 16 | dispatch(receiveAuth()); 17 | } 18 | dispatch(actions.fetchMessages(initialChannel)); 19 | dispatch(actions.fetchChannels(user.username)); 20 | } 21 | render() { 22 | return ( 23 | 24 | ); 25 | } 26 | } 27 | ChatContainer.propTypes = { 28 | messages: PropTypes.array.isRequired, 29 | user: PropTypes.object.isRequired, 30 | dispatch: PropTypes.func.isRequired, 31 | channels: PropTypes.array.isRequired, 32 | activeChannel: PropTypes.string.isRequired, 33 | typers: PropTypes.array.isRequired 34 | } 35 | 36 | function mapStateToProps(state) { 37 | return { 38 | messages: state.messages.data, 39 | channels: state.channels.data, 40 | activeChannel: state.activeChannel.name, 41 | user: state.auth.user, 42 | typers: state.typers, 43 | screenWidth: state.environment.screenWidth 44 | } 45 | } 46 | export default connect(mapStateToProps)(ChatContainer) 47 | -------------------------------------------------------------------------------- /src/common/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Exported from redux-devtools 4 | import { createDevTools } from 'redux-devtools'; 5 | 6 | // Monitors are separate packages, and you can make a custom one 7 | import LogMonitor from 'redux-devtools-log-monitor'; 8 | import DockMonitor from 'redux-devtools-dock-monitor'; 9 | 10 | // createDevTools takes a monitor and produces a DevTools component 11 | const DevTools = createDevTools( 12 | // Monitors are individually adjustable with props. 13 | // Consult their repositories to learn about those props. 14 | // Here, we put LogMonitor inside a DockMonitor. 15 | 17 | 18 | 19 | ); 20 | 21 | export default DevTools; 22 | -------------------------------------------------------------------------------- /src/common/css/chatapp.css: -------------------------------------------------------------------------------- 1 | 2 | a { 3 | text-decoration: none; 4 | } 5 | 6 | html, 7 | body, 8 | container { 9 | height: 100%; 10 | padding: 0; 11 | margin: 0; 12 | } 13 | 14 | .react { 15 | height: 100%; 16 | } 17 | 18 | .nav { 19 | margin: 0; 20 | color: white; 21 | background: #337ab7; 22 | width: 21rem; 23 | -ms-flex: 0 1rem; 24 | -webkit-box-flex: 0; 25 | -moz-box-flex: 0; 26 | -ms-box-flex: 0; 27 | box-flex: 0; 28 | display: flex; 29 | justify-content: flex-start; 30 | -webkit-box-orient: vertical; 31 | -moz-box-orient: vertical; 32 | -webkit-box-direction: normal; 33 | -moz-box-direction: normal; 34 | -webkit-flex-direction: column; 35 | -ms-flex-direction: column; 36 | flex-direction: column; 37 | } 38 | 39 | .main { 40 | margin: 0; 41 | background: #F5F8FF; 42 | -ms-flex: 1; 43 | -webkit-box-flex: 1; 44 | -moz-box-flex: 1; 45 | -ms-box-flex: 1; 46 | box-flex: 1; 47 | display: flex; 48 | flex-direction: column; 49 | justify-content: flex-start; 50 | align-content: flex-start; 51 | align-items: inherit; 52 | } 53 | 54 | span[class*="glyphicon-one-fine"] { 55 | padding-top: 0.46em; 56 | padding-right: 0.4em; 57 | } 58 | 59 | .glyphicon-one-fine-green-dot:before { 60 | content:"\25cf"; 61 | font-size: 0.7em; 62 | color: #34A853; 63 | } 64 | 65 | .glyphicon-one-fine-empty-dot:before { 66 | content:"\25cb"; 67 | font-size: 0.7em; 68 | } 69 | 70 | .btn-primary { 71 | border: none; 72 | } 73 | -------------------------------------------------------------------------------- /src/common/middleware/index.js: -------------------------------------------------------------------------------- 1 | export { default as promiseMiddleware } from '/promiseMiddleware'; 2 | -------------------------------------------------------------------------------- /src/common/middleware/promiseMiddleware.js: -------------------------------------------------------------------------------- 1 | // Middleware 2 | export default function promiseMiddleware() { 3 | return (next) => (action) => { 4 | const { promise, types, ...rest } = action; 5 | if (!promise) { 6 | return next(action); 7 | } 8 | const [REQUEST, SUCCESS, FAILURE] = types; 9 | next({ ...rest, type: REQUEST }); 10 | return promise.then( 11 | (result) => { 12 | next({ ...rest, result, type: SUCCESS }); 13 | }, 14 | (error) => { 15 | next({ ...rest, error, type: FAILURE }); 16 | } 17 | ); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/common/reducers/activeChannel.js: -------------------------------------------------------------------------------- 1 | import { CHANGE_CHANNEL } from '../constants/ActionTypes'; 2 | 3 | const initialState = { 4 | name: 'Lobby', 5 | id: 0 6 | }; 7 | 8 | export default function activeChannel(state = initialState, action) { 9 | switch (action.type) { 10 | case CHANGE_CHANNEL: 11 | return { 12 | name: action.channel.name, 13 | id: action.channel.id 14 | }; 15 | 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | AUTH_LOAD, 3 | AUTH_LOAD_SUCCESS, 4 | AUTH_LOAD_FAIL, 5 | AUTH_SIGNIN, 6 | AUTH_SIGNIN_SUCCESS, 7 | AUTH_SIGNIN_FAIL, 8 | AUTH_SIGNOUT, 9 | AUTH_SIGNOUT_SUCCESS, 10 | AUTH_SIGNOUT_FAIL, 11 | AUTH_SIGNUP, 12 | AUTH_SIGNUP_SUCCESS, 13 | AUTH_SIGNUP_FAIL, 14 | RECEIVE_SOCKET 15 | } from '../constants/ActionTypes'; 16 | 17 | const initialState = { 18 | loaded: false, 19 | user: { 20 | username: null, 21 | id: null, 22 | socketID: null 23 | } 24 | }; 25 | 26 | export default function auth(state = initialState, action = {}) { 27 | switch (action.type) { 28 | case AUTH_LOAD: 29 | return { 30 | ...state, 31 | loading: true 32 | }; 33 | case AUTH_LOAD_SUCCESS: 34 | return { 35 | ...state, 36 | loading: false, 37 | loaded: true, 38 | user: { ...state.user, username: action.user } 39 | }; 40 | case AUTH_LOAD_FAIL: 41 | return { 42 | ...state, 43 | loading: false, 44 | loaded: false, 45 | error: action.error 46 | }; 47 | case AUTH_SIGNIN: 48 | return { 49 | ...state, 50 | signingIn: true 51 | }; 52 | case AUTH_SIGNIN_SUCCESS: 53 | return { 54 | ...state, 55 | signingIn: false, 56 | user: { 57 | username: action.user.name, 58 | id: action.user.id 59 | } 60 | }; 61 | case AUTH_SIGNIN_FAIL: 62 | return { 63 | ...state, 64 | signingIn: false, 65 | user: { 66 | username: null, 67 | id: null 68 | }, 69 | signInError: action.error 70 | }; 71 | case AUTH_SIGNUP: 72 | return { 73 | ...state, 74 | signingUp: true 75 | }; 76 | case AUTH_SIGNUP_SUCCESS: 77 | return { 78 | ...state, 79 | signingUp: false, 80 | user: { 81 | username: action.newUser.name, 82 | id: action.newUser.id, 83 | socketID: null 84 | } 85 | }; 86 | case AUTH_SIGNUP_FAIL: 87 | return { 88 | ...state, 89 | user: { 90 | username: null, 91 | id: null 92 | } 93 | }; 94 | case AUTH_SIGNOUT: 95 | return { 96 | ...state, 97 | signingOut: true 98 | }; 99 | case AUTH_SIGNOUT_SUCCESS: 100 | return { 101 | ...state, 102 | signingOut: false, 103 | user: { 104 | username: null, 105 | id: null 106 | } 107 | }; 108 | case AUTH_SIGNOUT_FAIL: 109 | return { 110 | ...state, 111 | signingOut: false, 112 | signOutError: action.error 113 | }; 114 | 115 | case RECEIVE_SOCKET: 116 | return { 117 | ...state, 118 | user: {...state.user, 119 | socketID: action.socketID 120 | } 121 | }; 122 | default: 123 | return state; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/common/reducers/channels.js: -------------------------------------------------------------------------------- 1 | import { ADD_CHANNEL, RECEIVE_CHANNEL, LOAD_CHANNELS, LOAD_CHANNELS_SUCCESS, LOAD_CHANNELS_FAIL, AUTH_SIGNOUT_SUCCESS} from '../constants/ActionTypes'; 2 | 3 | const initialState = { 4 | loaded: false, 5 | data: [] 6 | }; 7 | 8 | export default function channels(state = initialState, action) { 9 | switch (action.type) { 10 | case ADD_CHANNEL: 11 | if (state.data.filter(channel => channel.name === action.channel.name).length !== 0) { 12 | return state; 13 | } 14 | return {...state, 15 | data: [...state.data, action.channel] 16 | }; 17 | case RECEIVE_CHANNEL: 18 | if (state.data.filter(channel => channel.name === action.channel.name).length !== 0) { 19 | return state; 20 | } 21 | return {...state, 22 | data: [...state.data, action.channel] 23 | }; 24 | case LOAD_CHANNELS: 25 | return {...state, 26 | loading: true 27 | }; 28 | case LOAD_CHANNELS_SUCCESS: 29 | return {...state, 30 | loading: false, 31 | loaded: true, 32 | data: [...state.data, ...action.json] 33 | }; 34 | case LOAD_CHANNELS_FAIL: 35 | return {...state, 36 | loading: false, 37 | loaded: false, 38 | error: action.error, 39 | data: [...state.data] 40 | }; 41 | case AUTH_SIGNOUT_SUCCESS: 42 | return { 43 | loaded: false, 44 | data: [] 45 | }; 46 | default: 47 | return state; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/common/reducers/environment.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | const initialState = { 4 | isMobile: false, 5 | screenHeight: null, 6 | screenWidth: null 7 | } 8 | 9 | export default function environment(state = initialState, action) { 10 | switch(action.type) { 11 | case types.CHANGE_IS_MOBILE: 12 | return { 13 | ...state, isMobile: action.isMobile 14 | } 15 | 16 | case types.CHANGE_WIDTH_AND_HEIGHT: 17 | return { 18 | ...state, screenHeight: action.screenHeight, screenWidth: action.screenWidth 19 | } 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import messages from './messages'; 2 | import channels from './channels'; 3 | import activeChannel from './activeChannel'; 4 | import auth from './auth'; 5 | import typers from './typers'; 6 | import welcomePage from './welcomePage'; 7 | import userValidation from './userValidation'; 8 | import environment from './environment'; 9 | import {combineReducers} from 'redux'; 10 | import {reducer as formReducer} from 'redux-form' 11 | 12 | const rootReducer = combineReducers({ 13 | messages, 14 | channels, 15 | activeChannel, 16 | auth, 17 | typers, 18 | welcomePage, 19 | userValidation, 20 | environment, 21 | formReducer 22 | }); 23 | 24 | export default rootReducer; 25 | -------------------------------------------------------------------------------- /src/common/reducers/messages.js: -------------------------------------------------------------------------------- 1 | import { ADD_MESSAGE, RECEIVE_MESSAGE, LOAD_MESSAGES, LOAD_MESSAGES_SUCCESS, LOAD_MESSAGES_FAIL, AUTH_SIGNOUT_SUCCESS} from '../constants/ActionTypes'; 2 | 3 | const initialState = { 4 | loaded: false, 5 | data: [], 6 | fetchHistory: [] 7 | }; 8 | export default function messages(state = initialState, action) { 9 | switch (action.type) { 10 | case ADD_MESSAGE: 11 | return {...state, 12 | data: [...state.data, action.message] 13 | }; 14 | case RECEIVE_MESSAGE: 15 | return {...state, 16 | data: [...state.data, action.message] 17 | }; 18 | case LOAD_MESSAGES: 19 | return {...state, 20 | loading: true 21 | }; 22 | case LOAD_MESSAGES_SUCCESS: 23 | return {...state, 24 | loading: false, 25 | loaded: true, 26 | fetchHistory: [...state.fetchHistory, { lastFetch: action.date, channelName: action.channel }], 27 | data: [...state.data.filter(message => message.channelID !== action.channel), ...action.json] 28 | }; 29 | case LOAD_MESSAGES_FAIL: 30 | return {...state, 31 | loading: false, 32 | loaded: false, 33 | error: action.error, 34 | data: [...state.data] 35 | }; 36 | case AUTH_SIGNOUT_SUCCESS: 37 | return { 38 | loaded: false, 39 | data: [], 40 | fetchHistory: [] 41 | }; 42 | default: 43 | return state; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/common/reducers/typers.js: -------------------------------------------------------------------------------- 1 | import { TYPING, STOP_TYPING} from '../constants/ActionTypes'; 2 | 3 | const initialState = []; 4 | export default function typers(state = initialState, action) { 5 | switch (action.type) { 6 | 7 | case TYPING: 8 | if (state.indexOf(action.username) === - 1) { 9 | return [...state, action.username]; 10 | } 11 | return state; 12 | case STOP_TYPING: 13 | return state.filter(user => 14 | user !== action.username 15 | ); 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/reducers/userValidation.js: -------------------------------------------------------------------------------- 1 | import { LOAD_USERVALIDATION, LOAD_USERVALIDATION_SUCCESS, LOAD_USERVALIDATION_FAIL} from '../constants/ActionTypes'; 2 | 3 | const initialState = { 4 | loaded: false, 5 | data: [] 6 | }; 7 | 8 | export default function userValidation(state = initialState, action) { 9 | switch (action.type) { 10 | case LOAD_USERVALIDATION: 11 | return {...state, 12 | loading: true 13 | }; 14 | case LOAD_USERVALIDATION_SUCCESS: 15 | return {...state, 16 | loading: false, 17 | loaded: true, 18 | data: action.json 19 | }; 20 | case LOAD_USERVALIDATION_FAIL: 21 | return {...state, 22 | loading: false, 23 | loaded: false, 24 | error: action.error, 25 | data: [...state.data] 26 | }; 27 | default: 28 | return state; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/common/reducers/welcomePage.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | const initialState = ''; 4 | export default function welcomePage(state = initialState, action) { 5 | switch (action.type) { 6 | 7 | case types.SAVE_USERNAME: 8 | return action.username; 9 | case types.AUTH_SIGNOUT_SUCCESS: 10 | return ''; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/common/routes.js: -------------------------------------------------------------------------------- 1 | import { Redirect, Router, Route, IndexRoute } from 'react-router'; 2 | import React from 'react'; 3 | 4 | import SignIn from './components/SignIn'; 5 | import ChatContainer from './containers/ChatContainer'; 6 | import SignUp from './components/SignUp'; 7 | import WelcomePage from './components/WelcomePage'; 8 | import App from './containers/App'; 9 | import {checkAuth} from './actions/authActions'; 10 | 11 | const requireAuth = (nextState, replace) => { 12 | if(!checkAuth()) { 13 | return replace(null, '/signin') 14 | } 15 | } 16 | const Routes = ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | export default Routes; 28 | -------------------------------------------------------------------------------- /src/common/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 2 | import promiseMiddleware from '../middleware/promiseMiddleware'; 3 | import DevTools from '../containers/DevTools'; 4 | import thunk from 'redux-thunk'; 5 | import rootReducer from '../reducers' 6 | 7 | const finalCreateStore = compose( 8 | applyMiddleware(thunk, promiseMiddleware), 9 | DevTools.instrument() 10 | )(createStore); 11 | 12 | export default function configureStore(initialState) { 13 | const store = finalCreateStore(rootReducer, initialState); 14 | 15 | if (module.hot) { 16 | // Enable Webpack hot module replacement for reducers 17 | module.hot.accept('../reducers', () => { 18 | const nextRootReducer = require('../reducers'); 19 | store.replaceReducer(nextRootReducer); 20 | }); 21 | } 22 | 23 | return store; 24 | } 25 | -------------------------------------------------------------------------------- /src/common/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.prod'); 3 | } else { 4 | module.exports = require('./configureStore.dev'); 5 | } 6 | -------------------------------------------------------------------------------- /src/common/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import rootReducer from '../reducers'; 3 | import thunk from 'redux-thunk'; 4 | import promiseMiddleware from '../middleware/promiseMiddleware'; 5 | 6 | const finalCreateStore = compose( 7 | applyMiddleware(thunk, promiseMiddleware) 8 | )(createStore); 9 | 10 | export default function configureStore(initialState) { 11 | return finalCreateStore(rootReducer, initialState); 12 | }; 13 | -------------------------------------------------------------------------------- /src/server/models/Channel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose'); 4 | 5 | var channelSchema = mongoose.Schema({ 6 | name: { type:String, unique: true }, 7 | id: String, 8 | private: Boolean, 9 | between: Array 10 | }); 11 | 12 | module.exports = mongoose.model('Channel', channelSchema); 13 | -------------------------------------------------------------------------------- /src/server/models/Message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose'); 4 | 5 | var messageSchema = mongoose.Schema({ 6 | id: String, 7 | channelID: String, 8 | text: String, 9 | user: Object, 10 | time: String 11 | }); 12 | 13 | module.exports = mongoose.model('Message', messageSchema); 14 | -------------------------------------------------------------------------------- /src/server/models/User.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcrypt-nodejs'); 2 | var mongoose = require('mongoose'); 3 | 4 | var UserSchema = mongoose.Schema({ 5 | local: { 6 | username: { type: String, unique: true }, 7 | password: String, 8 | email: String, 9 | }, 10 | facebook: { 11 | id: String, 12 | username: String, 13 | token: String, 14 | email: String, 15 | } 16 | }); 17 | 18 | UserSchema.methods.generateHash = function(password) { 19 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 20 | }; 21 | 22 | // checking if password is valid 23 | UserSchema.methods.validPassword = function(password) { 24 | return bcrypt.compareSync(password, this.local.password); 25 | }; 26 | 27 | module.exports = mongoose.model('User', UserSchema); 28 | -------------------------------------------------------------------------------- /src/server/routes/channel_routes.js: -------------------------------------------------------------------------------- 1 | var Channel = require('../models/Channel'); 2 | var bodyparser = require('body-parser'); 3 | 4 | module.exports = function(router) { 5 | router.use(bodyparser.json()); 6 | 7 | // deprecating this route since it just gets all channels 8 | router.get('/channels', function(req, res) { 9 | 10 | Channel.find({},{name: 1, id:1, _id:0}, function(err, data) { 11 | if(err) { 12 | console.log(err); 13 | return res.status(500).json({msg: 'internal server error'}); 14 | } 15 | 16 | res.json(data); 17 | }); 18 | }); 19 | 20 | // this route returns all channels including private channels for that user 21 | router.get('/channels/:name', function(req, res) { 22 | 23 | Channel.find({ $or: [ {between: req.params.name}, {private: false } ] }, {name: 1, id:1, private: 1, between: 1, _id:0}, function(err, data) { 24 | if(err) { 25 | console.log(err); 26 | return res.status(500).json({msg: 'internal server error'}); 27 | } 28 | 29 | res.json(data); 30 | }); 31 | }) 32 | 33 | // post a new user to channel list db 34 | router.post('/channels/new_channel', function(req, res) { 35 | var newChannel = new Channel(req.body); 36 | newChannel.save(function (err, data) { 37 | if(err) { 38 | console.log(err); 39 | return res.status(500).json({msg: 'internal server error'}); 40 | } 41 | 42 | res.json(data); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/server/routes/message_routes.js: -------------------------------------------------------------------------------- 1 | var Message = require('../models/Message'); 2 | var bodyparser = require('body-parser'); 3 | 4 | module.exports = function(router) { 5 | router.use(bodyparser.json()); 6 | 7 | // query DB for ALL messages 8 | router.get('/messages', function(req, res) { 9 | Message.find({}, {id: 1, channelID: 1, text: 1, user: 1, time: 1, _id: 0}, function(err, data) { 10 | if(err) { 11 | console.log(err); 12 | return res.status(500).json({msg: 'internal server error'}); 13 | } 14 | res.json(data); 15 | }); 16 | }); 17 | 18 | // query DB for messages for a specific channel 19 | router.get('/messages/:channel', function(req, res) { 20 | Message.find({channelID: req.params.channel}, {id: 1, channelID: 1, text: 1, user: 1, time: 1, _id: 0}, function(err, data) { 21 | if(err) { 22 | console.log(err); 23 | return res.status(500).json({msg: 'internal server error'}); 24 | } 25 | res.json(data); 26 | }); 27 | }) 28 | 29 | //post a new message to db 30 | router.post('/newmessage', function(req, res) { 31 | var newMessage = new Message(req.body); 32 | newMessage.save(function (err, data) { 33 | if(err) { 34 | console.log(err); 35 | return res.status(500).json({msg: 'internal server error'}); 36 | } 37 | res.json(data); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/server/routes/user_routes.js: -------------------------------------------------------------------------------- 1 | 'user strict'; 2 | 3 | var bodyparser = require('body-parser'); 4 | var User = require('../models/User.js'); 5 | 6 | module.exports = function loadUserRoutes(router, passport) { 7 | router.use(bodyparser.json()); 8 | 9 | router.get('/auth/facebook', passport.authenticate('facebook', { 10 | session: false, 11 | successRedirect: '/chat', 12 | failureRedirect: '/' 13 | })); 14 | 15 | router.get('/auth/facebook/callback', passport.authenticate('facebook', { 16 | session: false, 17 | successRedirect: '/chat', 18 | failureRedirect: '/' 19 | })); 20 | 21 | router.post('/sign_up', passport.authenticate('local-signup', { session: false}), function(req, res) { 22 | res.json(req.user); 23 | }); 24 | 25 | router.post('/sign_in', passport.authenticate('local-login', { session: false}), function(req, res) { 26 | res.json(req.user); 27 | }); 28 | 29 | router.get('/signout', function(req, res) { 30 | req.logout(); 31 | res.end(); 32 | }); 33 | 34 | //get auth credentials from server 35 | router.get('/load_auth_into_state', function(req, res) { 36 | res.json(req.user); 37 | }); 38 | 39 | // get usernames for validating whether a username is available 40 | router.get('/all_usernames', function(req, res) { 41 | User.find({'local.username': { $exists: true } }, {'local.username': 1, _id:0}, function(err, data) { 42 | if(err) { 43 | console.log(err); 44 | return res.status(500).json({msg: 'internal server error'}); 45 | } 46 | res.json(data); 47 | }); 48 | }) 49 | }; 50 | -------------------------------------------------------------------------------- /src/server/server.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import express from 'express'; 4 | import path from 'path'; 5 | 6 | import mongoose from 'mongoose'; 7 | 8 | import { renderToString } from 'react-dom/server' 9 | import { Provider } from 'react-redux' 10 | import React from 'react'; 11 | import configureStore from '../common/store/configureStore' 12 | import { RouterContext, match } from 'react-router'; 13 | import routes from '../common/routes'; 14 | import createHistory from 'history/createMemoryHistory' 15 | import DevTools from '../common/containers/DevTools'; 16 | import cors from 'cors'; 17 | import webpack from 'webpack'; 18 | import webpackConfig from '../../webpack.config.dev' 19 | const compiler = webpack(webpackConfig); 20 | import User from './models/User.js'; 21 | import passport from 'passport'; 22 | require('../../config/passport')(passport); 23 | import SocketIo from 'socket.io'; 24 | const app = express(); 25 | //set env vars 26 | process.env.MONGOLAB_URI = process.env.MONGOLAB_URI || 'mongodb://localhost/chat_dev'; 27 | process.env.PORT = process.env.PORT || 3000; 28 | 29 | // connect our DB 30 | mongoose.connect(process.env.MONGOLAB_URI); 31 | 32 | process.on('uncaughtException', function (err) { 33 | console.log(err); 34 | }); 35 | app.use(cors()); 36 | app.use(passport.initialize()); 37 | 38 | app.use(require('webpack-dev-middleware')(compiler, { 39 | noInfo: true, 40 | publicPath: webpackConfig.output.publicPath 41 | })); 42 | app.use(require('webpack-hot-middleware')(compiler)); 43 | 44 | //load routers 45 | const messageRouter = express.Router(); 46 | const usersRouter = express.Router(); 47 | const channelRouter = express.Router(); 48 | require('./routes/message_routes')(messageRouter); 49 | require('./routes/channel_routes')(channelRouter); 50 | require('./routes/user_routes')(usersRouter, passport); 51 | app.use('/api', messageRouter); 52 | app.use('/api', usersRouter); 53 | app.use('/api', channelRouter); 54 | 55 | app.use('/', express.static(path.join(__dirname, '..', 'static'))); 56 | 57 | app.get('/*', function(req, res) { 58 | const history = createHistory() 59 | const location = history.location 60 | match({ routes, location }, (err, redirectLocation, renderProps) => { 61 | 62 | const initialState = { 63 | auth: { 64 | user: { 65 | username: 'tester123', 66 | id: 0, 67 | socketID: null 68 | } 69 | } 70 | } 71 | const store = configureStore(initialState); 72 | // console.log(redirectLocation); 73 | // if(redirectLocation) { 74 | // return res.status(302).end(redirectLocation); 75 | // } 76 | 77 | 78 | if(err) { 79 | console.error(err); 80 | return res.status(500).end('Internal server error'); 81 | } 82 | 83 | if(!renderProps) { 84 | return res.status(404).end('Not found'); 85 | } 86 | const InitialView = ( 87 | 88 |
    89 | 90 | {process.env.NODE_ENV !== 'production' && } 91 |
    92 |
    93 | ); 94 | 95 | const finalState = store.getState(); 96 | const html = renderToString(InitialView) 97 | res.status(200).end(renderFullPage(html, finalState)); 98 | }) 99 | }) 100 | 101 | const server = app.listen(process.env.PORT, 'localhost', function(err) { 102 | if (err) { 103 | console.log(err); 104 | return; 105 | } 106 | console.log('server listening on port: %s', process.env.PORT); 107 | }); 108 | 109 | const io = new SocketIo(server, {path: '/api/chat'}) 110 | const socketEvents = require('./socketEvents')(io); 111 | 112 | function renderFullPage(html, initialState) { 113 | return ` 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | React Redux Socket.io Chat 123 | 124 | 125 | ${html} 126 | 129 | 130 | 131 | 132 | ` 133 | } 134 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); //enables ES6 ('import'.. etc) in Node 2 | if (process.env.NODE_ENV !== 'production') { 3 | require('./server.dev'); 4 | } else { 5 | require('./server.prod') 6 | } 7 | -------------------------------------------------------------------------------- /src/server/server.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import express from 'express'; 4 | import path from 'path'; 5 | 6 | import mongoose from 'mongoose'; 7 | 8 | import { renderToString } from 'react-dom/server' 9 | import { Provider } from 'react-redux' 10 | import React from 'react'; 11 | import configureStore from '../common/store/configureStore' 12 | import { RouterContext, match } from 'react-router'; 13 | import routes from '../common/routes'; 14 | import createHistory from 'history/createMemoryHistory' 15 | import cors from 'cors'; 16 | 17 | import User from './models/User.js'; 18 | import passport from 'passport'; 19 | require('../../config/passport')(passport); 20 | import SocketIo from 'socket.io'; 21 | const app = express(); 22 | 23 | //set env vars 24 | process.env.MONGOLAB_URI = process.env.MONGOLAB_URI || 'mongodb://localhost/chat_dev'; 25 | process.env.PORT = process.env.PORT || 3000; 26 | 27 | // connect our DB 28 | mongoose.connect(process.env.MONGOLAB_URI); 29 | 30 | process.on('uncaughtException', function (err) { 31 | console.log(err); 32 | }); 33 | 34 | app.use(cors()); 35 | app.use(passport.initialize()); 36 | 37 | //load routers 38 | const messageRouter = express.Router(); 39 | const usersRouter = express.Router(); 40 | const channelRouter = express.Router(); 41 | require('./routes/message_routes')(messageRouter); 42 | require('./routes/channel_routes')(channelRouter); 43 | require('./routes/user_routes')(usersRouter, passport); 44 | app.use('/api', messageRouter); 45 | app.use('/api', usersRouter); 46 | app.use('/api', channelRouter); 47 | 48 | app.use('/', express.static(path.join(__dirname, '../..', 'static'))); 49 | 50 | app.get('/*', function(req, res) { 51 | const history = createHistory() 52 | const location = history.location 53 | 54 | match({ routes, location }, (err, redirectLocation, renderProps) => { 55 | 56 | if(err) { 57 | console.error(err); 58 | return res.status(500).end('Internal server error'); 59 | } 60 | 61 | if(!renderProps) { 62 | return res.status(404).end('Not found'); 63 | } 64 | 65 | const store = configureStore(); 66 | 67 | const InitialView = ( 68 | 69 |
    70 | 71 |
    72 |
    73 | ); 74 | 75 | const initialState = store.getState(); 76 | const html = renderToString(InitialView) 77 | res.status(200).end(renderFullPage(html, initialState)); 78 | }) 79 | }) 80 | 81 | const server = app.listen(process.env.PORT, function(err) { 82 | if (err) { 83 | console.log(err); 84 | return; 85 | } 86 | console.log('server listening on port: %s', process.env.PORT); 87 | }); 88 | 89 | const io = new SocketIo(server, {path: '/api/chat'}) 90 | const socketEvents = require('./socketEvents')(io); 91 | 92 | function renderFullPage(html, initialState) { 93 | return ` 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | React Redux Socket.io Chat 103 | 104 | 105 | ${html} 106 | 109 | 110 | 111 | 112 | ` 113 | } 114 | -------------------------------------------------------------------------------- /src/server/socketEvents.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function(io) { 2 | io.on('connection', function(socket) { 3 | socket.join('Lobby'); 4 | socket.on('chat mounted', function(user) { 5 | // TODO: Does the server need to know the user? 6 | socket.emit('receive socket', socket.id) 7 | }) 8 | socket.on('leave channel', function(channel) { 9 | socket.leave(channel) 10 | }) 11 | socket.on('join channel', function(channel) { 12 | socket.join(channel.name) 13 | }) 14 | socket.on('new message', function(msg) { 15 | socket.broadcast.to(msg.channelID).emit('new bc message', msg); 16 | }); 17 | socket.on('new channel', function(channel) { 18 | socket.broadcast.emit('new channel', channel) 19 | }); 20 | socket.on('typing', function (data) { 21 | socket.broadcast.to(data.channel).emit('typing bc', data.user); 22 | }); 23 | socket.on('stop typing', function (data) { 24 | socket.broadcast.to(data.channel).emit('stop typing bc', data.user); 25 | }); 26 | socket.on('new private channel', function(socketID, channel) { 27 | socket.broadcast.to(socketID).emit('receive private channel', channel); 28 | }) 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raineroviir/react-redux-socketio-chat/0739e285243a06a8ecef504d885735c9bf268acc/static/favicon.ico -------------------------------------------------------------------------------- /test/TestUtils.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {applyMiddleware} from 'redux'; 3 | import thunkMiddleware from 'redux-thunk'; 4 | import promiseMiddleware from '../src/common/middleware/promiseMiddleware'; 5 | 6 | const middlewares = [ thunkMiddleware, promiseMiddleware ]; 7 | 8 | export function mockStore(getState, expectedActions, onLastAction) { 9 | if (!Array.isArray(expectedActions)) { 10 | throw new Error('expectedActions should be an array'); 11 | } 12 | 13 | if (onLastAction !== undefined && typeof onLastAction !== 'function') { 14 | throw new Error('onLastAction should be undefined or function'); 15 | } 16 | 17 | function mockStoreWithoutMiddleware() { 18 | return { 19 | getState() { 20 | return typeof getState === 'function' ? getState() : getState; 21 | }, 22 | 23 | dispatch(action) { 24 | const expectedAction = expectedActions.shift(); 25 | expect(action).toEqual(expectedAction); 26 | if (onLastAction && !expectedActions.length) { 27 | onLastAction(); 28 | } 29 | return action; 30 | } 31 | }; 32 | } 33 | 34 | const mockStoreWithMiddleware = applyMiddleware( 35 | ...middlewares 36 | )(mockStoreWithoutMiddleware); 37 | 38 | return mockStoreWithMiddleware(); 39 | } 40 | -------------------------------------------------------------------------------- /test/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import * as actions from '../src/common/actions/actions'; 3 | import * as types from '../src/common/constants/ActionTypes'; 4 | 5 | describe('actions', () => { 6 | it('should receive a message', () => { 7 | const message = 'Test'; 8 | const expectedAction = { 9 | type: types.RECEIVE_MESSAGE, 10 | message 11 | } 12 | 13 | expect(actions.receiveRawMessage(message)).toEqual(expectedAction); 14 | }); 15 | 16 | it('should receive a channel', () => { 17 | const channel = 'Test'; 18 | const expectedAction = { 19 | type: types.RECEIVE_CHANNEL, 20 | channel 21 | } 22 | 23 | expect(actions.receiveRawChannel(channel)).toEqual(expectedAction); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/asyncActions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { applyMiddleware } from 'redux'; 3 | import * as actions from '../src/common/actions/actions'; 4 | import * as types from '../src/common/constants/ActionTypes'; 5 | import nock from 'nock'; 6 | import thunk from 'redux-thunk'; 7 | import promiseMiddleware from '../src/common/middleware/promiseMiddleware'; 8 | 9 | import configureStore from 'redux-mock-store'; 10 | 11 | const middlewares = [ thunk, promiseMiddleware ]; 12 | const mockStore = configureStore(middlewares); 13 | 14 | // Test in mocha 15 | 16 | // describe('async actions', () => { 17 | // afterEach(() => { 18 | // nock.cleanAll() 19 | // }) 20 | 21 | // it('creates LOAD_MESSAGES_SUCCESS when fetching messages has been done', (done) => { 22 | // const expectedActions = [ 23 | // { type: types.LOAD_MESSAGES }, 24 | // { type: types.LOAD_MESSAGES_SUCCESS, body: { messages: ['do something'] } } 25 | // ] 26 | // const store = mockStore({ messages: [] }, expectedActions, done) 27 | // store.dispatch(actions.fetchMessages()) 28 | // }) 29 | // }) 30 | -------------------------------------------------------------------------------- /test/reducers.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import reducer from '../src/common/reducers/messages'; 3 | import * as types from '../src/common/constants/ActionTypes'; 4 | 5 | // describe('add message reducer', () => { 6 | // const initialState = { loaded: false, data: [] }; 7 | 8 | // it('should return the initial state', () => { 9 | // expect(reducer(undefined, {}) 10 | // ).toEqual(initialState) 11 | // }) 12 | 13 | // it('should handle ADD_MESSAGE', () => { 14 | // expect(reducer(initialState, { 15 | // type: types.ADD_MESSAGE, 16 | // message: { 17 | // channelID: 0, 18 | // text: 'testing 101', 19 | // user: 'TestMan', 20 | // time: 500 21 | // } 22 | // }) 23 | // ).toEqual({ 24 | // data: [{ 25 | // id: 0, 26 | // channelID: 0, 27 | // text: 'testing 101', 28 | // user: 'TestMan', 29 | // time: 500 30 | // }], 31 | // loaded: false 32 | // }) 33 | // }) 34 | 35 | 36 | // }) 37 | -------------------------------------------------------------------------------- /test/shallowRendering.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import expect from 'expect'; 4 | 5 | const CoolComponent = ({greeting}) => ( 6 |
    7 |

    Greeting

    8 |
    {greeting}
    9 |
    10 | ) 11 | 12 | describe('CoolComponent', () => { 13 | 14 | it('should...', () => { 15 | const renderer = TestUtils.createRenderer(); 16 | renderer.render(); 17 | const output = renderer.getRenderOutput(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'inline-source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'webpack-hot-middleware/client', 9 | './src/client/index' 10 | ], 11 | output: { 12 | path: path.resolve(__dirname, './static/dist'), 13 | filename: 'bundle.js', 14 | publicPath: '/dist/' 15 | }, 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin(), 19 | new webpack.DefinePlugin({ 20 | 'process.env': { 21 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') 22 | } 23 | }) 24 | ], 25 | module: { 26 | loaders: [ 27 | { 28 | test: /\.js$/, 29 | loader: 'babel', 30 | query: { 31 | plugins: [ 32 | [ 33 | 'react-transform', { 34 | transforms: [{ 35 | transform: 'react-transform-hmr', 36 | imports: ['react'], 37 | locals: ['module'] 38 | }, { 39 | transform: 'react-transform-catch-errors', 40 | imports: ['react', 'redbox-react'] 41 | }] 42 | } 43 | ] 44 | ] 45 | }, 46 | include: [path.resolve(__dirname, 'src')] 47 | }, 48 | { 49 | test: /\.css?$/, 50 | loaders: ['style', 'raw'] 51 | } 52 | ] 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var CleanPlugin = require('clean-webpack-plugin'); 4 | 5 | module.exports = { 6 | devtool: 'source-map', 7 | entry: [ 8 | './src/client/index' 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, './static/dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new CleanPlugin(['./static/dist'], {verbose: true}), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') 20 | } 21 | }), 22 | new webpack.optimize.OccurenceOrderPlugin(), 23 | new webpack.optimize.DedupePlugin(), 24 | new webpack.optimize.UglifyJsPlugin({ 25 | compressor: { 26 | warnings: false 27 | } 28 | }) 29 | ], 30 | module: { 31 | loaders: [{ 32 | test: /\.js$/, 33 | loader: 'babel', 34 | include: [path.resolve(__dirname, 'src')] 35 | }, 36 | { 37 | test: /\.css?$/, 38 | loaders: ['style', 'raw'] 39 | }] 40 | } 41 | }; 42 | --------------------------------------------------------------------------------