├── .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 | 
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 | 
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 |
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 |
154 | {filteredChannels.map(channel =>
155 | {
156 | return msg.channelID === channel.name;
157 | }).length} channel={channel} key={channel.id} onClick={::this.handleChangeChannel} />
158 | )}
159 |
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 |
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 |
139 |
140 |
141 | );
142 | const bigNav = (
143 |
144 | {dropDownMenu}
145 |
148 |
149 | );
150 | return (
151 |
152 | {screenWidth < 500 ? mobileNav : bigNav }
153 |
154 |
159 | {PrivateMessageModal}
160 |
161 | {filteredMessages.map(message =>
162 |
163 | )}
164 |
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 |
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 |
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 |
97 |
98 |
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 |
43 |
44 |
64 | Or
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 | return (
73 |
74 |
81 |
82 |
83 |
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 |
--------------------------------------------------------------------------------